Seja Bem-vindo
Carregando dados...
Buscando barbeiros, serviços e datas disponíveis
Entrar na sua conta
🛒 Seu Carrinho
📅 Adicionar à Agenda
Para não esquecer, salve seu horário:
Serviço
Barbeiro
Data
Horário
Agora não, obrigado
"); janelaWhats.document.close(); } catch(e) {} } swalL("Finalizando venda..."); gsr.call("salvarPedidoLoja", [{ itens: itensSalvar, total: total.toFixed(2).replace(".", ","), taxaEntrega: taxa > 0 ? taxa.toFixed(2).replace(".", ",") : "", obs: obs, entrega: _carrinhoEntrega, pagamento: _carrinhoPagamento, whatsapp: WHATS_LOJA, data: new Date().toLocaleDateString("pt-BR"), nomeCliente: nomeCliente, telefoneCliente: telefoneCliente, endereco: enderecoEntrega }]) .then(function(res) { if (typeof Swal !== "undefined") Swal.close(); if (!res || res.sucesso === false) { throw new Error((res && res.erro) || "Não foi possível salvar o pedido."); } var numReal = res.numero || String(Date.now()).slice(-5); var msg = "PEDIDO #" + numReal + LF; msg += SEP + LF; msg += "Cliente: " + nomeCliente + LF; msg += "WhatsApp: " + telefoneCliente + LF; msg += SEP + LF + LF; carrinho.forEach(function(item, idx) { var p = item.produto; var preco = _getPreco(p); var sub = (preco * item.qty).toFixed(2).replace(".", ","); msg += p.nome + LF; msg += " " + item.qty + "x R$ " + preco.toFixed(2).replace(".", ",") + " = R$ " + sub + LF; if (idx < carrinho.length - 1) msg += LF; }); msg += LF + SEP + LF; msg += "Subtotal: R$ " + subtotal.toFixed(2).replace(".", ",") + LF; if (taxa > 0) msg += "Taxa de entrega: R$ " + taxa.toFixed(2).replace(".", ",") + LF; msg += "TOTAL: R$ " + total.toFixed(2).replace(".", ",") + LF; msg += SEP + LF + LF; msg += "Entrega: " + tipoEntrega + LF; if (enderecoEntrega) msg += "Endereco: " + enderecoEntrega + LF; msg += "Pagamento: " + tipoPag; if (_carrinhoPagamento === "pix" && _pixChaveGlobal) msg += LF + "Chave PIX: " + _pixChaveGlobal; if (obs) msg += LF + LF + "Obs: " + obs; var urlWhats = "https://wa.me/" + WHATS_LOJA + "?text=" + encodeURIComponent(msg); limparCarrinho(); try { fecharCarrinhoModal(); } catch(e) {} if (janelaWhats && !janelaWhats.closed) { try { janelaWhats.location.href = urlWhats; } catch(e) { window.open(urlWhats, "_blank"); } } else { window.open(urlWhats, "_blank"); } if (typeof Swal !== "undefined") { var pushInfo = res.pushEnviado === false ? "
⚠️ O pedido foi salvo, mas a notificação push não encontrou uma inscrição ativa. Abra o painel da loja e ative as notificações." : ""; Swal.fire({ toast: true, position: "top-end", icon: "success", title: "Pedido #" + numReal + " finalizado!", html: pushInfo, showConfirmButton: false, timer: pushInfo ? 5000 : 3500, background: "#0e1220", color: "#e8edf8" }); } if (typeof carregarFinLoja === "function") setTimeout(carregarFinLoja, 500); }) .catch(function(err) { if (typeof Swal !== "undefined") Swal.close(); if (janelaWhats && !janelaWhats.closed) { try { janelaWhats.close(); } catch(e) {} } if (typeof Swal !== "undefined") { Swal.fire({ icon: "error", title: "Erro ao finalizar venda", text: err && err.message ? err.message : "Não foi possível salvar o pedido.", confirmButtonText: "OK" }); } }); } function _renderBannerDestaque(lista) { window._renderBannerDestaque(lista); } window._bannerClick = function(e) { if (e) e.stopPropagation(); // Usa _sliderProdutos exposto via window pelo IIFE de _renderBannerDestaque const slides = window._sliderProdutos || []; if (slides.length === 0) return; // Descobre índice do slide ativo pelo dot (mais confiável que transform) const wrap = document.getElementById('loja-banner-wrap'); let slideIdx = 0; if (wrap) { const dots = wrap.querySelectorAll('.loja-banner-dot'); dots.forEach(function(d, i) { if (d.classList.contains('active')) slideIdx = i; }); } const p = slides[slideIdx] || slides[0]; if (!p) return; carrinhoAdicionarProduto(p); _atualizarQtyNoGrid(); if (typeof Swal !== 'undefined') { Swal.fire({ toast:true, position:'top-end', icon:'success', title:'✅ Adicionado ao carrinho!', text: p.nome, showConfirmButton:false, timer:1800, background:'#0e1220', color:'#e8edf8' }); } }; /* ── BUSCA ── */ function buscarLoja(termo) { _termoBusca = (termo || '').trim().toLowerCase(); const clearBtn = document.getElementById('loja-search-clear'); if (clearBtn) clearBtn.classList.toggle('visible', _termoBusca.length > 0); _aplicarFiltros(); } function limparBuscaLoja() { _termoBusca = ''; const inp = document.getElementById('loja-search-input'); if (inp) inp.value = ''; const clearBtn = document.getElementById('loja-search-clear'); if (clearBtn) clearBtn.classList.remove('visible'); _aplicarFiltros(); } function _aplicarFiltros() { let lista = _produtosCache; if (_catAtual) lista = lista.filter(p => p.categoria === _catAtual); if (_termoBusca) lista = lista.filter(p => (p.nome || '').toLowerCase().includes(_termoBusca) || (p.descricao || '').toLowerCase().includes(_termoBusca) || (p.categoria || '').toLowerCase().includes(_termoBusca) ); if (_termoBusca && lista.length === 0) { document.getElementById('loja-grid').innerHTML = `
🔍
Nenhum produto encontrado para "${_termoBusca}"
`; } else { renderLoja(lista); // Atualiza banner com produtos filtrados (ou todos se sem filtro) const slidesSource = (_catAtual || _termoBusca) ? lista : _produtosCache; _renderBannerDestaque(slidesSource.length ? slidesSource : _produtosCache); } } function carregarLoja() { document.getElementById('loja-loading').style.display = 'block'; document.getElementById('loja-grid').innerHTML = ''; // Carrega carrinho salvo do localStorage _carregarCarrinhoLocal(); // [SUBSTITUÍDO] api.run → gsr.call Promise.all([ gsr.call('getProdutosPublicos'), gsr.call('getCategoriasLoja') ]) .then(([lista, cats]) => { document.getElementById('loja-loading').style.display = 'none'; _produtosCache = lista || []; _categoriasLojaPublicas = (cats && cats.length) ? cats : []; _montarFiltrosLoja(); _renderBannerDestaque(_produtosCache); renderLoja(_produtosCache); // Auto-abrir produto via link compartilhado (?produto=NomeDoProduto) (function() { try { var _pParam = new URLSearchParams(window.location.search).get('produto'); if (_pParam) { var _pIdx = _produtosCache.findIndex(function(x) { return x.nome === _pParam; }); if (_pIdx >= 0) { mostrarSecao('loja'); setTimeout(function() { abrirProdutoModalIdx(_pIdx); }, 400); } } } catch(e) {} })(); }) .catch(() => { document.getElementById('loja-loading').style.display = 'none'; document.getElementById('loja-grid').innerHTML = '
⚠️ Não foi possível carregar os produtos.
'; }); } function _montarFiltrosLoja() { // Usa categorias cadastradas se disponíveis, senão extrai dos produtos const cats = (_categoriasLojaPublicas && _categoriasLojaPublicas.length) ? _categoriasLojaPublicas : [...new Set(_produtosCache.map(p => p.categoria).filter(Boolean))].sort(); const wrap = document.getElementById('loja-filtros'); wrap.innerHTML = '' + cats.map(c => ``).join(''); } var _categoriasLojaPublicas = []; function filtrarLoja(cat) { _catAtual = cat; document.querySelectorAll('.loja-filtro-btn').forEach(b => b.classList.toggle('active', b.dataset.cat === cat)); _aplicarFiltros(); } window._lojaIndexada = {}; function renderLoja(lista) { const grid = document.getElementById('loja-grid'); window._lojaIndexada = {}; if (!lista.length) { grid.innerHTML = '
Nenhum produto disponível no momento.
'; return; } grid.innerHTML = ''; let i = 0; function lote() { let html = ''; for (let x = 0; x < 6 && i < lista.length; x++, i++) { const p = lista[i]; window._lojaIndexada[i] = p; const preco = _getPreco(p); const carrinhoIdx = carrinho.findIndex(c => c.produto.nome === p.nome); const qtyAtual = carrinhoIdx >= 0 ? carrinho[carrinhoIdx].qty : 0; const imgHtml = p.imagem ? `` : `
🛒
`; const promoHtml = (p.emPromocao && p.valorPromo > 0) ? `R$ ${p.valor.toFixed(2).replace('.', ',')}` : ''; const estoqueNum = _estoqueDisponivel(p); const esgotado = estoqueNum !== null && estoqueNum <= 0; const addBtnStyle = (qtyAtual > 0 || esgotado) ? 'display:none' : ''; html += `
${imgHtml}
${esgotado ? '
🚫 ESGOTADO
' : (p.emPromocao && p.valorPromo > 0) ? '
🔥 PROMO
' : ''}
${p.nome}
${p.categoria || ''}
${promoHtml}R$ ${preco.toFixed(2).replace('.', ',')}
${esgotado ? '' : `
🛒 Adicionar
`}
${qtyAtual || 0}
`; } grid.insertAdjacentHTML('beforeend', html); if (i < lista.length) requestAnimationFrame(lote); } lote(); } function _onCardClick(e, i) { if (e.target.closest('.produto-card-add') || e.target.closest('.produto-card-qty')) return; abrirProdutoModalIdx(i); } function _onAddClick(e, i) { e.stopPropagation(); const p = window._lojaIndexada[i]; if (!p) return; carrinhoAdicionarProduto(p); _atualizarQtyNoGrid(); } function _qtyCard(delta, i, e) { e.stopPropagation(); const p = window._lojaIndexada[i]; if (!p) return; const idx = carrinho.findIndex(c => c.produto.nome === p.nome); if (idx < 0) return; const novaQty = carrinho[idx].qty + delta; if (delta > 0) { const estoque = _estoqueDisponivel(p); if (estoque !== null && novaQty > estoque) { if (typeof Swal !== 'undefined') { Swal.fire({ toast: true, position: 'top-end', icon: 'warning', title: 'Estoque insuficiente', showConfirmButton: false, timer: 2000, background: '#0e1220', color: '#e8edf8' }); } return; } } if (novaQty <= 0) carrinho.splice(idx, 1); else carrinho[idx].qty = novaQty; _atualizarCarrinhoUI(); _atualizarQtyNoGrid(); } function abrirProdutoModalIdx(i) { const p = window._lojaIndexada[i]; if (p) { _produtoModalAtual = p; abrirProdutoModal(p); } } // ── Galeria do modal ── var _pmGaleriaFotos = [], _pmGaleriaIdx = 0; function pmGaleriaNav(dir) { if (_pmGaleriaFotos.length < 2) return; _pmGaleriaIdx = (_pmGaleriaIdx + dir + _pmGaleriaFotos.length) % _pmGaleriaFotos.length; _pmGaleriaAtualizar(); } function _pmGaleriaAtualizar() { var img = document.getElementById('pm-img'); if (img) img.src = _pmGaleriaFotos[_pmGaleriaIdx]; var dots = document.getElementById('pm-gal-dots'); if (dots) { Array.from(dots.children).forEach(function(d, i) { d.style.background = i === _pmGaleriaIdx ? 'var(--accent)' : 'rgba(255,255,255,0.25)'; }); } } function abrirProdutoModal(p) { _produtoModalAtual = p; const preco = _getPreco(p); const galeriaEl = document.getElementById('pm-galeria'); const img = document.getElementById('pm-img'); const ph = document.getElementById('pm-img-placeholder'); const prevBtn = document.getElementById('pm-gal-prev'); const nextBtn = document.getElementById('pm-gal-next'); const dotsEl = document.getElementById('pm-gal-dots'); // Monta array de fotos (imagem principal + extras) _pmGaleriaFotos = []; if (p.imagem) _pmGaleriaFotos.push(p.imagem); if (p.imagem2) _pmGaleriaFotos.push(p.imagem2); if (p.imagem3) _pmGaleriaFotos.push(p.imagem3); if (p.imagem4) _pmGaleriaFotos.push(p.imagem4); _pmGaleriaIdx = 0; if (_pmGaleriaFotos.length > 0) { galeriaEl.style.display = ''; ph.style.display = 'none'; img.src = _pmGaleriaFotos[0]; var multi = _pmGaleriaFotos.length > 1; prevBtn.style.display = multi ? 'flex' : 'none'; nextBtn.style.display = multi ? 'flex' : 'none'; dotsEl.style.display = multi ? 'flex' : 'none'; if (multi) { dotsEl.innerHTML = _pmGaleriaFotos.map(function(_, i) { return ''; }).join(''); } } else { galeriaEl.style.display = 'none'; ph.style.display = ''; } document.getElementById('pm-nome').textContent = p.nome; document.getElementById('pm-cat').textContent = p.categoria || ''; document.getElementById('pm-desc').textContent = p.descricao || ''; document.getElementById('pm-preco').textContent = 'R$ ' + preco.toFixed(2).replace('.', ','); const precoDeEl = document.getElementById('pm-preco-de'); const badgeEl = document.getElementById('pm-badge'); if (p.emPromocao && p.valorPromo > 0) { precoDeEl.textContent = 'R$ ' + p.valor.toFixed(2).replace('.', ','); badgeEl.style.display = ''; } else { precoDeEl.textContent = ''; badgeEl.style.display = 'none'; } const msgWa = encodeURIComponent('Olá! Tenho interesse no produto: *' + p.nome + '* - R$ ' + preco.toFixed(2).replace('.', ',')); const whatsEl = document.getElementById('pm-whats'); whatsEl.href = 'https://wa.me/' + WHATS_LOJA + '?text=' + msgWa; whatsEl.style.display = 'flex'; const btnAddEl = document.getElementById('pm-btn-add'); const estoqueModal = _estoqueDisponivel(p); if (btnAddEl) { if (estoqueModal !== null && estoqueModal <= 0) { btnAddEl.textContent = '🚫 Esgotado'; btnAddEl.disabled = true; btnAddEl.style.opacity = '0.5'; btnAddEl.style.cursor = 'not-allowed'; } else { btnAddEl.textContent = '🛒 Adicionar ao carrinho'; btnAddEl.disabled = false; btnAddEl.style.opacity = ''; btnAddEl.style.cursor = ''; } } document.getElementById('produtoModal').classList.add('open'); document.body.style.overflow = 'hidden'; } function adicionarAoCarrinhoPorModal() { if (!_produtoModalAtual) return; const estoque = _estoqueDisponivel(_produtoModalAtual); if (estoque !== null && estoque <= 0) { if (typeof Swal !== 'undefined') { Swal.fire({ toast: true, position: 'top-end', icon: 'warning', title: 'Produto esgotado', showConfirmButton: false, timer: 2000, background: '#0e1220', color: '#e8edf8' }); } return; } carrinhoAdicionarProduto(_produtoModalAtual); _atualizarQtyNoGrid(); fecharProdutoModal(); Swal.fire({ toast:true, position:'top-end', icon:'success', title:'✅ Adicionado ao carrinho!', text:_produtoModalAtual ? _produtoModalAtual.nome : '', showConfirmButton:false, timer:1800, background:'#0e1220', color:'#e8edf8' }); } function fecharProdutoModal() { document.getElementById('produtoModal').classList.remove('open'); document.body.style.overflow = ''; _produtoModalAtual = null; } function fecharProdutoModalFora(e) { if (e.target === document.getElementById('produtoModal')) fecharProdutoModal(); } // ══════════════════════════════════════════════════════════════ // COMPARTILHAMENTO DE PRODUTO // ══════════════════════════════════════════════════════════════ function compartilharProdutoWhats() { var p = _produtoModalAtual; if (!p) return; var preco = _getPreco(p); var precoFmt = 'R$ ' + preco.toFixed(2).replace('.', ','); var baseUrl = window.location.href.split('?')[0]; var url = baseUrl + '?produto=' + encodeURIComponent(p.nome); var txt = '*' + p.nome + '*\n'; if (p.descricao) txt += p.descricao.slice(0, 120) + (p.descricao.length > 120 ? '...' : '') + '\n'; if (p.emPromocao && p.valorPromo > 0) { var original = 'R$ ' + p.valor.toFixed(2).replace('.', ','); txt += '🔥 PROMOÇÃO! De ' + original + ' por *' + precoFmt + '*\n'; } else { txt += 'Preço: *' + precoFmt + '*\n'; } txt += '\n🛒 Ver produto: ' + url; var waUrl = 'https://wa.me/?text=' + encodeURIComponent(txt); window.open(waUrl, '_blank'); } function gerarPostInstagram() { var p = _produtoModalAtual; if (!p) return; var preco = _getPreco(p); var precoFmt = 'R$ ' + preco.toFixed(2).replace('.', ','); var nomeLoja = (document.title.split(/s*[—-]s*/)[0] || '').trim() || 'GC IMPORTES'; var canvas = document.getElementById('pm-canvas-insta'); var ctx = canvas.getContext('2d'); var W = 1080, H = 1080; canvas.width = W; canvas.height = H; function _rrect(x, y, w, h, r) { ctx.beginPath(); ctx.moveTo(x + r, y); ctx.lineTo(x + w - r, y); ctx.quadraticCurveTo(x + w, y, x + w, y + r); ctx.lineTo(x + w, y + h - r); ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h); ctx.lineTo(x + r, y + h); ctx.quadraticCurveTo(x, y + h, x, y + h - r); ctx.lineTo(x, y + r); ctx.quadraticCurveTo(x, y, x + r, y); ctx.closePath(); } function _desenharPost(imgObj, logoObj) { var grad = ctx.createLinearGradient(0, 0, W, H); grad.addColorStop(0, '#060810'); grad.addColorStop(0.5, '#0e1220'); grad.addColorStop(1, '#060810'); ctx.fillStyle = grad; ctx.fillRect(0, 0, W, H); var glow = ctx.createRadialGradient(W/2, 0, 0, W/2, 0, W * 0.65); glow.addColorStop(0, 'rgba(79,142,247,0.18)'); glow.addColorStop(1, 'rgba(79,142,247,0)'); ctx.fillStyle = glow; ctx.fillRect(0, 0, W, H); ctx.strokeStyle = 'rgba(79,142,247,0.06)'; ctx.lineWidth = 1; for (var gx = 0; gx < W; gx += 60) { ctx.beginPath(); ctx.moveTo(gx, 0); ctx.lineTo(gx, H); ctx.stroke(); } for (var gy = 0; gy < H; gy += 60) { ctx.beginPath(); ctx.moveTo(0, gy); ctx.lineTo(W, gy); ctx.stroke(); } var imgY = 100, imgSize = 500; var imgX = (W - imgSize) / 2; if (imgObj) { ctx.save(); _rrect(imgX, imgY, imgSize, imgSize, 24); ctx.clip(); ctx.drawImage(imgObj, imgX, imgY, imgSize, imgSize); ctx.restore(); } else { ctx.fillStyle = 'rgba(79,142,247,0.12)'; _rrect(imgX, imgY, imgSize, imgSize, 24); ctx.fill(); ctx.fillStyle = 'rgba(79,142,247,0.5)'; ctx.font = '80px sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText('?', W/2, imgY + imgSize/2); } ctx.strokeStyle = 'rgba(79,142,247,0.3)'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(80, 640); ctx.lineTo(W - 80, 640); ctx.stroke(); var isPromo = p.emPromocao && p.valorPromo > 0; if (isPromo) { ctx.fillStyle = 'rgba(248,113,113,0.2)'; _rrect(W/2 - 90, 655, 180, 44, 22); ctx.fill(); ctx.strokeStyle = 'rgba(248,113,113,0.5)'; ctx.lineWidth = 1.5; _rrect(W/2 - 90, 655, 180, 44, 22); ctx.stroke(); ctx.fillStyle = '#f87171'; ctx.font = 'bold 24px sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText('* PROMOCAO *', W/2, 677); } ctx.fillStyle = '#c8d4ee'; ctx.textAlign = 'center'; ctx.textBaseline = 'top'; var nomeY = isPromo ? 715 : 660; var maxW2 = W - 120; var nomeTxt = p.nome; ctx.font = '500 38px sans-serif'; if (ctx.measureText(nomeTxt).width > maxW2) { var words = nomeTxt.split(' '), l1 = '', l2 = ''; for (var wi = 0; wi < words.length; wi++) { var test = l1 + (l1 ? ' ' : '') + words[wi]; if (ctx.measureText(test).width <= maxW2) { l1 = test; } else { l2 = words.slice(wi).join(' '); break; } } ctx.fillText(l1, W/2, nomeY); if (l2) ctx.fillText(l2, W/2, nomeY + 46); nomeY += l2 ? 92 : 46; } else { ctx.fillText(nomeTxt, W/2, nomeY); nomeY += 46; } if (p.descricao) { ctx.fillStyle = '#5a6480'; ctx.font = '400 28px sans-serif'; var descTxt = p.descricao.length > 80 ? p.descricao.slice(0, 80) + '...' : p.descricao; ctx.fillText(descTxt, W/2, nomeY + 6); nomeY += 40; } ctx.font = 'bold 88px sans-serif'; ctx.fillStyle = '#4f8ef7'; ctx.textBaseline = 'top'; var precoY = nomeY + 20; ctx.fillText(precoFmt, W/2, precoY); if (isPromo) { var originalFmt = 'R$ ' + p.valor.toFixed(2).replace('.', ','); ctx.font = '36px sans-serif'; ctx.fillStyle = '#5a6480'; ctx.textBaseline = 'top'; var oldW2 = ctx.measureText(originalFmt).width; var oldX = W/2 - oldW2/2; ctx.fillText(originalFmt, oldX, precoY + 96); ctx.strokeStyle = '#5a6480'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(oldX, precoY + 96 + 20); ctx.lineTo(oldX + oldW2, precoY + 96 + 20); ctx.stroke(); } ctx.fillStyle = 'rgba(14,18,32,0.85)'; ctx.fillRect(0, H - 110, W, 110); ctx.strokeStyle = 'rgba(79,142,247,0.25)'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(0, H - 110); ctx.lineTo(W, H - 110); ctx.stroke(); if (logoObj) { // Desenha logo centralizada no rodapé var lH = 70, lW = Math.min(lH * (logoObj.naturalWidth / logoObj.naturalHeight || 1), 400); ctx.drawImage(logoObj, (W - lW) / 2, H - 55 - lH / 2, lW, lH); } else { ctx.fillStyle = '#4f8ef7'; ctx.font = 'bold 38px sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(nomeLoja.toUpperCase(), W/2, H - 55); } var link = document.createElement('a'); link.download = 'post-instagram-' + (p.nome || 'produto').replace(/s+/g, '-').toLowerCase() + '.png'; link.href = canvas.toDataURL('image/png'); link.click(); Swal.fire({ toast:true, position:'top-end', icon:'success', title:'Imagem gerada!', text:'Salva como PNG - pronta para o Instagram', showConfirmButton:false, timer:2800, background:'#0e1220', color:'#e8edf8' }); } var _logoSrc = (document.getElementById('logo-principal') || {}).src || ''; function _carregarEDesenhar(imgProd) { if (_logoSrc && !_logoSrc.includes('1QM5pnj3z')) { var imgLogo = new Image(); imgLogo.crossOrigin = 'anonymous'; imgLogo.onload = function() { _desenharPost(imgProd, imgLogo); }; imgLogo.onerror = function() { _desenharPost(imgProd, null); }; imgLogo.src = _logoSrc; } else { _desenharPost(imgProd, null); } } if (p.imagem) { var img2 = new Image(); if (!p.imagem.startsWith('data:')) img2.crossOrigin = 'anonymous'; img2.onload = function() { _carregarEDesenhar(img2); }; img2.onerror = function() { _carregarEDesenhar(null); }; img2.src = p.imagem; } else { _carregarEDesenhar(null); } } // ══════════════════════════════════════════════════════════════ // POST INSTAGRAM — versão admin (não depende do modal aberto) function gerarPostInstagramAdmin(idx) { var lista = typeof produtosCache !== 'undefined' ? produtosCache : []; var p = lista.find(function(x) { return x.idx === idx; }); if (!p) { Swal.fire({ toast:true, position:'top-end', icon:'error', title:'Produto não encontrado', showConfirmButton:false, timer:2000, background:'#0e1220', color:'#e8edf8' }); return; } var preco = p.emPromocao && p.valorPromo > 0 ? p.valorPromo : p.valor; var precoFmt = 'R$ ' + preco.toFixed(2).replace('.', ','); var nomeLoja = _nomeBarbearia || (document.title.split(/s*[—-]s*/)[0] || '').trim(); if (!nomeLoja || nomeLoja.toLowerCase() === 'painel') nomeLoja = 'GC IMPORTES'; var canvas = document.createElement('canvas'); var ctx = canvas.getContext('2d'); var W = 1080, H = 1080; canvas.width = W; canvas.height = H; function _rrect(x,y,w,h,r){ctx.beginPath();ctx.moveTo(x+r,y);ctx.lineTo(x+w-r,y);ctx.quadraticCurveTo(x+w,y,x+w,y+r);ctx.lineTo(x+w,y+h-r);ctx.quadraticCurveTo(x+w,y+h,x+w-r,y+h);ctx.lineTo(x+r,y+h);ctx.quadraticCurveTo(x,y+h,x,y+h-r);ctx.lineTo(x,y+r);ctx.quadraticCurveTo(x,y,x+r,y);ctx.closePath();} function _desenhar(imgObj, logoObj) { var grad=ctx.createLinearGradient(0,0,W,H); grad.addColorStop(0,'#060810');grad.addColorStop(0.5,'#0e1220');grad.addColorStop(1,'#060810'); ctx.fillStyle=grad;ctx.fillRect(0,0,W,H); var glow=ctx.createRadialGradient(W/2,0,0,W/2,0,W*0.65); glow.addColorStop(0,'rgba(79,142,247,0.18)');glow.addColorStop(1,'rgba(79,142,247,0)'); ctx.fillStyle=glow;ctx.fillRect(0,0,W,H); ctx.strokeStyle='rgba(79,142,247,0.06)';ctx.lineWidth=1; for(var gx=0;gx0; if(isPromo){ctx.fillStyle='rgba(248,113,113,0.2)';_rrect(W/2-90,655,180,44,22);ctx.fill();ctx.strokeStyle='rgba(248,113,113,0.5)';ctx.lineWidth=1.5;_rrect(W/2-90,655,180,44,22);ctx.stroke();ctx.fillStyle='#f87171';ctx.font='bold 24px sans-serif';ctx.textAlign='center';ctx.textBaseline='middle';ctx.fillText('* PROMOCAO *',W/2,677);} ctx.fillStyle='#c8d4ee';ctx.textAlign='center';ctx.textBaseline='top'; var nomeY=isPromo?715:660,maxW2=W-120,nomeTxt=p.nome; ctx.font='500 38px sans-serif'; if(ctx.measureText(nomeTxt).width>maxW2){var words=nomeTxt.split(' '),l1='',l2='';for(var wi=0;wi SESSION_TTL) { localStorage.removeItem(SESSION_KEY); return null; } return s.email; } catch(e) { return null; } } function sessaoLimpar() { try { localStorage.removeItem(SESSION_KEY); } catch(e) {} } // Chave de tema isolada por slug (mesma lógica do painel) const _TEMA_KEY_LOJA = 'painel_tema_' + _slug; function aplicarTema(tema) { const temaVal = (tema && tema !== 'undefined' && tema !== 'null') ? tema : 'padrao'; document.documentElement.removeAttribute('data-tema'); if (temaVal !== 'padrao') document.documentElement.setAttribute('data-tema', temaVal); try { localStorage.setItem(_TEMA_KEY_LOJA, temaVal); } catch(e) {} } (function() { try { aplicarTema(localStorage.getItem(_TEMA_KEY_LOJA) || 'padrao'); } catch(e) {} })(); // ══════════════════════════════════════════════════════════════ // CONSTANTES // ══════════════════════════════════════════════════════════════ const MESES=["Janeiro","Fevereiro","Março","Abril","Maio","Junho","Julho","Agosto","Setembro","Outubro","Novembro","Dezembro"]; const DIAS=["Dom","Seg","Ter","Qua","Qui","Sex","Sáb"]; // Garante que o foco seja removido antes de qualquer Swal abrir, // evitando o aviso: "Blocked aria-hidden on a focused element" (function(){ const _origFire = Swal.fire.bind(Swal); Swal.fire = function(...args) { // Blur em qualquer elemento focado dentro do #app antes de abrir modal const focused = document.activeElement; if (focused && typeof focused.blur === 'function' && focused !== document.body) { focused.blur(); } // Garante também que o #app não tenha aria-hidden residual ao abrir const appEl = document.getElementById('app'); if (appEl && appEl.getAttribute('aria-hidden') === 'true') { appEl.removeAttribute('aria-hidden'); } const result = _origFire(...args); // Após fechar o modal, remove aria-hidden do #app novamente if (result && typeof result.then === 'function') { result.then(() => { if (appEl && appEl.getAttribute('aria-hidden') === 'true') { appEl.removeAttribute('aria-hidden'); } }).catch(() => {}); } return result; }; })(); // Fix definitivo: intercepta qualquer ganho de foco dentro de elemento com // aria-hidden="true" e blura imediatamente — resolve race-condition de timing // com SweetAlert2 independentemente de qual botão foi clicado. (function(){ // Barreira de última linha: dispara APÓS o foco ser setado (capture=true), // impedindo o aviso WAI-ARIA mesmo quando o browser re-foca o botão depois do blur. document.addEventListener('focusin', function(e) { var el = e.target; while (el && el !== document.body) { if (el.getAttribute && el.getAttribute('aria-hidden') === 'true') { if (el.id === 'app') { // O #app nunca deve bloquear foco — remove o atributo e deixa continuar el.removeAttribute('aria-hidden'); return; } // Outros elementos com aria-hidden (ex: modais): faz blur normalmente e.target.blur(); return; } el = el.parentElement; } }, true); // Observer: remove aria-hidden do #app SEMPRE que for adicionado. // O #app nunca deve ter aria-hidden=true enquanto está visível para o usuário. // O SweetAlert2 adiciona esse atributo mas o remove sozinho ao fechar — // porém há race conditions; este observer garante a remoção imediata. function fixAriaHidden() { var appEl = document.getElementById('app'); if (!appEl) return; // Remove imediatamente se já tiver if (appEl.getAttribute('aria-hidden') === 'true') { appEl.removeAttribute('aria-hidden'); } // Observer que remove SEMPRE que o atributo aparecer new MutationObserver(function() { if (appEl.getAttribute('aria-hidden') === 'true') { appEl.removeAttribute('aria-hidden'); } }).observe(appEl, { attributes: true, attributeFilter: ['aria-hidden'] }); } if (document.getElementById('app')) fixAriaHidden(); else document.addEventListener('DOMContentLoaded', fixAriaHidden); })(); const SW=(t,x,i)=>Swal.fire({title:t,text:x||'',icon:i||'info',background:'#0e1220',color:'#e8edf8',confirmButtonColor:'#4f8ef7'}); const SWL=(t)=>Swal.fire({title:t||'Aguarde...',didOpen:()=>Swal.showLoading(),background:'#0e1220',color:'#e8edf8'}); // ══════════════════════════════════════════════════════════════ // NAVEGAÇÃO // ══════════════════════════════════════════════════════════════ function mostrarSecao(s) { if (s === 'loja') { const pill = document.getElementById('pill-loja'); if (pill && pill.style.display === 'none') return; } document.querySelectorAll('.secao').forEach(e => e.classList.remove('ativa')); document.querySelectorAll('.nav-pill').forEach(e => e.classList.remove('active')); const wrapper = document.querySelector('.wrapper'); if (wrapper) wrapper.classList.toggle('loja-ativa', s === 'loja'); document.getElementById('secao-' + s).classList.add('ativa'); document.getElementById('pill-' + s).classList.add('active'); const taglines = { agendar:'Agendar horário', loja:'Nossa Loja', cliente:'Meus Agendamentos' }; document.getElementById('header-tagline').textContent = taglines[s] || ''; if (s === 'loja' && !_produtosCache.length) carregarLoja(); const fab = document.getElementById('carrinhoFab'); if (s === 'loja' && _carrinhoQtdTotal() > 0) fab.classList.add('visible'); else fab.classList.remove('visible'); } function mostrarSubCli(s) { ['login','cadastro','area'].forEach(k => document.getElementById('cli-'+k).style.display = k===s?'block':'none'); } // ══════════════════════════════════════════════════════════════════ // CACHE MULTI-CAMADA // ══════════════════════════════════════════════════════════════════ const CACHE = { VISUAL: { key: 'barb_visual_v3_' + _slug, ttl: 5 * 60 * 1000 }, ESTATICO: { key: 'barb_estatico_v3_' + _slug, ttl: 2 * 60 * 1000 }, DATAS: { key: 'barb_datas_v3_' + _slug, ttl: 1 * 60 * 1000 }, }; function _cacheSalvar(camada, dados) { try { localStorage.setItem(camada.key, JSON.stringify({ ts: Date.now(), dados })); } catch(e) {} } function _cacheLer(camada) { try { const raw = localStorage.getItem(camada.key); if (!raw) return null; const c = JSON.parse(raw); if (!c || Date.now() - c.ts > camada.ttl) return null; return c.dados; } catch(e) { return null; } } function _cacheLimpar(camada) { try { localStorage.removeItem(camada.key); } catch(e) {} } function _aplicarVisualInstantaneo() { const visual = _cacheLer(CACHE.VISUAL); if (!visual) return; const _eLojaCached = !!window.__FORCE_LOJA__; const _logoVisual = (_eLojaCached ? (visual.logoLoja || visual.logoUrl) : visual.logoUrl); if (_logoVisual) { const logoEl = document.getElementById('logo-principal'); if (logoEl) logoEl.src = _logoVisual; document.querySelectorAll('.brand-logo').forEach(img => { img.src = _logoVisual; }); } if (visual.logoFooterUrl) { const footerEl = document.getElementById('logo-footer'); if (footerEl) footerEl.src = visual.logoFooterUrl; } if (visual.tema) aplicarTema(visual.tema); if (visual.nomeBarbearia) { document.title = visual.nomeBarbearia + ' — Agendamento'; } if (visual.whatsappLoja) { WHATS_LOJA = visual.whatsappLoja.trim().replace(/D/g, ''); _atualizarLinksWhats(WHATS_LOJA); } } // ══════════════════════════════════════════════════════════════════ // INIT PRINCIPAL // ══════════════════════════════════════════════════════════════════ function carregarTudoIniciais() { _aplicarVisualInstantaneo(); const cacheEstatico = _cacheLer(CACHE.ESTATICO); const cacheDatas = _cacheLer(CACHE.DATAS); if (cacheEstatico) { // ── Verifica bloqueio total SEMPRE, mesmo com cache ───────────── // (licença pode ter vencido +10 dias desde o último carregamento) gsr.call('getInicialCompleto').then(function(res) { if (res && res.sistema_bloqueado) { const loadingBlock = document.getElementById('init-loading-block'); if (loadingBlock) loadingBlock.style.display = 'none'; const wrapper = document.querySelector('.wrapper'); if (wrapper) { const tel = (res.cfg && res.cfg.whatsappLoja) || (res.barbearia && res.barbearia.whatsapp_loja) || ''; const num = tel.replace(/D/g,''); const waBtn = tel ? '📲 Falar no WhatsApp' : ''; wrapper.innerHTML = '
' + '
✂️
' + '
Atendimento Pausado
' + '
Nossa agenda está temporariamente indisponível para novos agendamentos online. Entre em contato diretamente para mais informações.
' + waBtn + '
'; } } }).catch(function() { /* silencioso */ }); // ── Fim verificação bloqueio ───────────────────────────────────── _aplicarDadosEstaticos(cacheEstatico); if (cacheDatas) { // FIX: dados do cache localStorage foram salvos sem duração de serviço específica. // Aplica apenas datasDisponiveis (quais datas existem), sem popular vagasPorData, // para evitar mostrar contagem de vagas errada antes do serviço ser selecionado. // recarregarDatasParaBarbeiro vai popular vagasPorData corretamente depois. const _cDatasArr = typeof cacheDatas[0] === 'object' ? cacheDatas.map(r => r.data) : cacheDatas; datasDisponiveis = _cDatasArr; renderCalendar(); _atualizarDatasBackground(); } else { _buscarDatasServidor(); } setTimeout(_atualizarEstaticosBackground, 3000); } else { _buscarTudoServidor(); } } // [SUBSTITUÍDO] api.run → gsr.call // ── Só faz requisição se o cache estático estiver mais velho que 5 minutos ── function _atualizarEstaticosBackground() { try { const raw = localStorage.getItem(CACHE.ESTATICO.key); if (raw) { const c = JSON.parse(raw); // Se o cache ainda tem menos de 5 min, não re-busca if (c && (Date.now() - c.ts) < 5 * 60 * 1000) return; } } catch(e) {} gsr.call('getInicialCompleto') .then(res => { if (!res) return; _cacheSalvar(CACHE.VISUAL, { logoUrl: res.cfg?.logoUrl || '', logoFooterUrl: res.cfg?.logoFooterUrl || '', tema: res.cfg?.tema || 'padrao', nomeBarbearia: res.cfg?.nomeBarbearia || '', whatsappLoja: res.cfg?.whatsappLoja || '', taxaEntrega: res.cfg?.taxaEntrega || '', pixChave: res.cfg?.pixChave || '', pixChaveBarbearia: res.cfg?.pixChaveBarbearia || '', logoLoja: res.cfg?.logoLoja || '', whatsappBarbearia: res.cfg?.whatsappBarbearia || '', }); _cacheSalvar(CACHE.ESTATICO, { cfg: res.cfg, barbeiros: res.barbeiros, servicos: res.servicos, tiposServico: res.tiposServico || [], temAgendamento: res.temAgendamento, temLoja: res.temLoja, }); }) .catch(() => {}); } // [SUBSTITUÍDO] api.run → gsr.call function _buscarTudoServidor() { gsr.call('getInicialCompleto') .then(res => { // ── Bloqueio total: licença vencida há mais de 10 dias ────────── if (res && res.sistema_bloqueado) { const loadingBlock = document.getElementById('init-loading-block'); if (loadingBlock) loadingBlock.style.display = 'none'; const wrapper = document.querySelector('.wrapper'); if (wrapper) { const tel = (res.cfg && res.cfg.whatsappLoja) || (res.barbearia && res.barbearia.whatsapp_loja) || ''; const num = tel.replace(/D/g,''); const waBtn = tel ? '📲 Falar no WhatsApp' : ''; wrapper.innerHTML = '
' + '
✂️
' + '
Atendimento Pausado
' + '
Nossa agenda está temporariamente indisponível para novos agendamentos online. Entre em contato diretamente para mais informações.
' + waBtn + '
'; } return; // Interrompe o fluxo normal } // ── Fim bloqueio ──────────────────────────────────────────────── _cacheSalvar(CACHE.VISUAL, { logoUrl: res.cfg?.logoUrl || '', logoFooterUrl: res.cfg?.logoFooterUrl || '', tema: res.cfg?.tema || 'padrao', nomeBarbearia: res.cfg?.nomeBarbearia || '', whatsappLoja: res.cfg?.whatsappLoja || '', logoLoja: res.cfg?.logoLoja || '', whatsappBarbearia: res.cfg?.whatsappBarbearia || '', }); _cacheSalvar(CACHE.ESTATICO, { cfg: res.cfg, barbeiros: res.barbeiros, servicos: res.servicos, tiposServico: res.tiposServico || [], temAgendamento: res.temAgendamento, temLoja: res.temLoja, }); if (res.datas && res.datas.length) { _cacheSalvar(CACHE.DATAS, res.datas); } _aplicarDadosEstaticos(res); if (res.datas && res.datas.length) { _aplicarDatas(res.datas); } else { // getInicialCompleto não retornou datas — busca separadamente _buscarDatasServidor(); } }) .catch(() => { document.getElementById('init-loading-block').style.display = 'none'; document.getElementById('init-erro').style.display = 'block'; }); } // [SUBSTITUÍDO] api.run → gsr.call function _buscarDatasServidor() { const b0 = barbeiros[0]; if (!b0) return; const chave = (b0.email && b0.email.trim()) ? b0.email.trim() : b0.nome; // ✅ FIX: passa duração do serviço atualmente selecionado para que a API // calcule vagas usando a duração real (e não apenas o intervalo de slot). // Se nenhum serviço está selecionado ainda, usa 0 (API usa bMin como padrão). const svcEl = document.getElementById('servico'); const svc = svcEl && servicos[svcEl.value]; const duracaoSvc = svc ? Number(svc.tempo) : 0; gsr.call('getDatasDisponiveisBarbeiro', [chave, duracaoSvc]) .then(lista => { const datas = lista || []; if (datas.length) _cacheSalvar(CACHE.DATAS, datas); _aplicarDatas(datas); }) .catch(() => { renderCalendar(); }); } // [SUBSTITUÍDO] api.run → gsr.call // ── Só faz requisição se o cache de datas estiver mais velho que 90 segundos ── function _atualizarDatasBackground() { const b0 = barbeiros[0]; if (!b0) return; const chave = (b0.email && b0.email.trim()) ? b0.email.trim() : b0.nome; // Verifica idade do cache antes de disparar try { const raw = localStorage.getItem(CACHE.DATAS.key); if (raw) { const c = JSON.parse(raw); if (c && (Date.now() - c.ts) < 90 * 1000) return; // ✅ FIX: reduzido de 3min para 90s } } catch(e) {} setTimeout(() => { // ✅ FIX: passa duração do serviço atualmente selecionado para que a API // calcule vagas usando a duração real (e não apenas o intervalo de slot). const svcEl = document.getElementById('servico'); const svc = svcEl && servicos[svcEl.value]; const duracaoSvc = svc ? Number(svc.tempo) : 0; gsr.call('getDatasDisponiveisBarbeiro', [chave, duracaoSvc]) .then(lista => { const datas = lista || []; if (!datas.length) return; _cacheSalvar(CACHE.DATAS, datas); const novas = typeof datas[0] === 'object' ? datas.map(r => r.data) : datas; const diff = novas.length !== datasDisponiveis.length || novas.some(d => !datasDisponiveis.includes(d)); if (diff) { // FIX: se a busca usou duracaoSvc=0 (nenhum serviço selecionado) mas o usuário // já escolheu um serviço, não sobrescreve vagasPorData — apenas sincroniza // quais datas estão disponíveis (sem alterar a contagem de vagas por duração). const svcAtual = document.getElementById('servico'); const temSvcSelecionado = svcAtual && servicos[svcAtual.value] && Number(servicos[svcAtual.value].tempo) > 0; if (duracaoSvc === 0 && temSvcSelecionado) { // Atualiza apenas datasDisponiveis, preserva vagasPorData correto datasDisponiveis = novas; renderCalendar(); } else { _aplicarDatas(datas); } } }) .catch(() => {}); }, 2000); } function _aplicarDadosEstaticos(res) { aplicarConfigsBarbearia(res.cfg); barbeiros = res.barbeiros || []; servicos = res.servicos || []; const sB = document.getElementById('barbeiro'); sB.innerHTML = ''; barbeiros.forEach((b, i) => { const o = document.createElement('option'); o.value = i; o.textContent = b.nome; sB.appendChild(o); }); _tiposServicoIndex = res.tiposServico || []; document.getElementById('grid-barbeiros').dataset.aberto = 'true'; if (barbeiros.length === 1) { // Só um barbeiro: seleciona automático e puxa os serviços dele _barbeiroEscolhido = true; sB.value = 0; document.getElementById('grid-barbeiros').dataset.aberto = 'false'; renderGridBarbeiros(); _indexarTiposServicos(res.tiposServico || []); const b0 = barbeiros[0]; const chave0 = (b0.email && b0.email.trim()) ? b0.email.trim() : b0.nome; recarregarServicosParaBarbeiro(chave0); } else { // Vários barbeiros: nada pré-selecionado, lista de serviços só depois da escolha _barbeiroEscolhido = false; sB.value = ''; servicos = []; renderGridBarbeiros(); _indexarTiposServicos(res.tiposServico || []); _popularServicos(res.tiposServico || []); } _aplicarFeaturesIndex(res.temAgendamento !== false, res.temLoja === true); } function _aplicarDatas(lista) { if (!lista || !lista.length) { renderCalendar(); return; } if (typeof lista[0] === 'object') { datasDisponiveis = lista.map(r => r.data); lista.forEach(r => { vagasPorData[r.data] = r.vagas; }); } else { datasDisponiveis = lista; } renderCalendar(); } function recarregarDados() { document.getElementById('init-erro').style.display = 'none'; document.getElementById('init-loading-block').style.display = 'block'; _cacheLimpar(CACHE.ESTATICO); _cacheLimpar(CACHE.DATAS); _buscarTudoServidor(); } const PLANOS_COM_LOJA = ['completo']; // [SUBSTITUÍDO] api.run → gsr.call (duas chamadas aninhadas) function _verificarPlanoLoja(barbeiros) { if (!barbeiros || !barbeiros.length) { _aplicarFeaturesIndex(false, false); return; } const emailReferencia = barbeiros[0].email || barbeiros[0].nome || ''; if (!emailReferencia) { _aplicarFeaturesIndex(false, false); return; } gsr.call('getPlanoFeaturesConfig') .then(cfg => { if (!cfg || !cfg.planos) { const plano = (cfg || '').toString().toLowerCase(); _aplicarFeaturesIndex(true, ['completo'].includes(plano)); return; } Promise.all([ gsr.call('getProdutosPublicos'), gsr.call('getCategoriasLoja'), gsr.call('getConfigsBarbearia') ]) .then(([lista, cats, cfg]) => { if (cfg) aplicarConfigsBarbearia(cfg); const pl = (plano || 'basico').toLowerCase(); const pf = (perfil || '').toLowerCase(); const planoFeatures = cfg.planos[pl] || cfg.planos['basico'] || []; const perfilFeatures = (cfg.perfis && cfg.perfis[pf]) ? cfg.perfis[pf] : []; const features = [...new Set([...planoFeatures, ...perfilFeatures])]; // Permissão liberada pela planilha (qualquer das duas chaves ativa a loja) const temAgendamento = features.includes('index-agendamento'); const temLoja = features.includes('index-loja') || features.includes('cfg-loja'); _aplicarFeaturesIndex(temAgendamento, temLoja); }) .catch(() => { _aplicarFeaturesIndex(true, false); }); }) .catch(() => { _aplicarFeaturesIndex(true, false); }); } function _aplicarFeaturesIndex(temAgendamento, temLoja) { // ── Rota /loja injetada pelo Worker ────────────────────────── if (window.__FORCE_LOJA__) { temLoja = true; temAgendamento = false; // esconde aba Agendar na /loja } const secaoAgen = document.getElementById('secao-agendar'); const pillAgen = document.getElementById('pill-agendar'); const agenContent = document.getElementById('agendar-content'); const initBlock = document.getElementById('init-loading-block'); if (initBlock) initBlock.style.display = 'none'; const navPills = document.getElementById('nav-pills-wrap'); if (navPills) navPills.style.display = ''; if (agenContent) agenContent.style.display = temAgendamento ? 'flex' : 'none'; if (!temAgendamento) { if (pillAgen) pillAgen.style.display = 'none'; if (secaoAgen) secaoAgen.style.display = 'none'; } else { if (pillAgen) pillAgen.style.display = ''; if (secaoAgen) secaoAgen.style.display = ''; } if (temLoja) { _mostrarAbaLoja(); } else { _ocultarAbaLoja(); } if (!temAgendamento && !temLoja) { mostrarSecao('cliente'); } else if (!temAgendamento && temLoja) { const pillCliente = document.getElementById('pill-cliente'); if (pillCliente) pillCliente.style.display = 'none'; mostrarSecao('loja'); } else { mostrarSecao('agendar'); } if (clienteEmail) _aplicarDadosLogado(); } function _ocultarAbaLoja() { const pill=document.getElementById('pill-loja'), secao=document.getElementById('secao-loja'); if (pill) pill.style.display='none'; if (secao) secao.style.display='none'; if (pill && pill.classList.contains('active')) mostrarSecao('agendar'); } function _mostrarAbaLoja() { const pill=document.getElementById('pill-loja'), secao=document.getElementById('secao-loja'); if (pill) pill.style.display=''; if (secao) secao.style.display=''; } // [SUBSTITUÍDO] api.run → gsr.call const _cacheServPorBarb = {}; // { email: { ts, lista } } const _TTL_SERV_BARB = 5 * 60 * 1000; // 5 minutos function recarregarServicosParaBarbeiro(emailBarbeiro) { const _tc = (typeof _tiposServicoIndex !== 'undefined' && _tiposServicoIndex.length) ? _tiposServicoIndex : undefined; if (!emailBarbeiro) { _popularServicos(_tc); return; } const agora = Date.now(); const hit = _cacheServPorBarb[emailBarbeiro]; // ✅ Cache válido → usa direto, sem requisição if (hit && (agora - hit.ts) < _TTL_SERV_BARB) { servicos = hit.lista; _indexarTiposServicos(_tc); _popularServicos(_tc); return; } // 🔄 Cache expirado ou inexistente → busca no servidor gsr.call('getServicosBarbeiro', [emailBarbeiro]) .then(lista => { const filtrada = (lista || []).filter(s => s.ativo !== false); _cacheServPorBarb[emailBarbeiro] = { ts: agora, lista: filtrada }; servicos = filtrada; _indexarTiposServicos(_tc); _popularServicos(_tc); }) .catch(() => { _indexarTiposServicos(_tc); _popularServicos(_tc); }); } function _popularServicos(tiposConfig) { // Nova carga de serviços → cliente precisa escolher novamente _servicoEscolhido = false; _atualizarBloqueioCalendario(); const sS = document.getElementById('servico'); sS.innerHTML = ''; const tiposGrupos = {}; servicos.forEach(s => { const tipo = s.tipo || 'Serviço'; if (!tiposGrupos[tipo]) tiposGrupos[tipo] = []; tiposGrupos[tipo].push(s); }); // Ordena os tipos pela ordem configurada; tipos sem configuração vão para o final const configMap = {}; if (tiposConfig && tiposConfig.length) { tiposConfig.forEach((t, i) => { configMap[t.nome] = t.ordem != null ? t.ordem : 9999 + i; }); } const tiposOrdem = Object.keys(tiposGrupos).sort((a, b) => { const oa = configMap[a] != null ? configMap[a] : 9999; const ob = configMap[b] != null ? configMap[b] : 9999; return oa - ob; }); tiposOrdem.forEach(tipo => { const grp = document.createElement('optgroup'); grp.label = tipo; tiposGrupos[tipo].forEach(s => { const idx = servicos.indexOf(s); const o = document.createElement('option'); o.value = idx; o.textContent = s.nome; grp.appendChild(o); }); sS.appendChild(grp); }); renderGridServicos(tiposConfig); atualizarResumo(); } // ══════════════════════════════════════════════════════════════ // GRID BARBEIROS // ══════════════════════════════════════════════════════════════ function renderGridBarbeiros() { const grid=document.getElementById('grid-barbeiros'); const selIdx=parseInt(document.getElementById('barbeiro').value); const aberto=grid.dataset.aberto==='true'; grid.innerHTML=''; barbeiros.forEach((b,i)=>{ if(!aberto&&i!==selIdx)return; const card=document.createElement('div'); card.className='pick-card'+(i===selIdx?' selected':''); const imgHtml = (b.foto||b.imagem) ? ``:`
✂️
`; const trocarHtml=(!aberto&&i===selIdx)?`
Trocar ▾
`:''; card.innerHTML=imgHtml+`
${b.nome}
Barbeiro
${trocarHtml}`; card.onclick=()=>{ if(!aberto){grid.dataset.aberto='true';renderGridBarbeiros();} else{document.getElementById('barbeiro').value=i;_barbeiroEscolhido=true;grid.dataset.aberto='false';renderGridBarbeiros();onBarbeiroChange();} }; grid.appendChild(card); }); if(aberto){const guia=document.createElement('div');guia.style.cssText='width:100%;font-size:0.75rem;color:var(--muted);text-align:center;padding:4px 0;';guia.textContent='Toque no barbeiro para selecionar';grid.insertBefore(guia,grid.firstChild);} } // ══════════════════════════════════════════════════════════════ // GRID SERVIÇOS // ══════════════════════════════════════════════════════════════ function _indexarTiposServicos(tiposConfig) { // Fallback palette/ícones para tipos sem configuração const PALETTE = ['#4f8ef7','#a78bfa','#34d399','#f472b6','#fb923c','#38bdf8','#fbbf24']; const ICONES = ['✂️','🎯','📦','💈','⚡','🌟','💎']; let idx = 0; // Indexa cores/ícones a partir dos tipos configurados no banco (cor e icone reais) if (tiposConfig && tiposConfig.length) { tiposConfig.forEach(t => { if (t.nome) { _TIPO_COR_MAP[t.nome] = t.cor || PALETTE[idx % PALETTE.length]; _TIPO_ICONE_MAP[t.nome] = t.icone || ICONES[idx % ICONES.length]; idx++; } }); } // Para serviços cujo tipo não tem configuração, usa a palette de fallback servicos.forEach(s => { const tipo = s.tipo || 'Serviço'; if (!_TIPO_COR_MAP[tipo]) { _TIPO_COR_MAP[tipo] = PALETTE[idx % PALETTE.length]; _TIPO_ICONE_MAP[tipo] = ICONES[idx % ICONES.length]; idx++; } }); } function _getTipoCor(tipo) { return _TIPO_COR_MAP[tipo] || '#4f8ef7'; } function _getTipoIcone(tipo) { return _TIPO_ICONE_MAP[tipo] || '✂️'; } // ── Controla bloqueio visual do calendário quando serviço não foi escolhido ── function _atualizarBloqueioCalendario() { const cardData = document.getElementById('card-data'); const trigger = document.getElementById('svc-trigger'); const bloqEl = document.getElementById('cal-servico-aviso'); if (!cardData) return; if (!_servicoEscolhido) { // Destaca o trigger de serviço como obrigatório if (trigger) { trigger.style.borderColor = 'var(--warning)'; trigger.style.boxShadow = '0 0 0 1px rgba(251,191,36,0.35)'; } // Exibe aviso sobre o calendário if (!bloqEl) { const aviso = document.createElement('div'); aviso.id = 'cal-servico-aviso'; aviso.style.cssText = 'display:flex;align-items:center;gap:10px;padding:10px 14px;margin-bottom:10px;' + 'background:rgba(251,191,36,0.08);border:1px solid rgba(251,191,36,0.3);' + 'border-radius:10px;font-size:0.82rem;color:var(--warning);cursor:pointer;'; aviso.innerHTML = '⚠️ Escolha o serviço primeiro — toque acima para selecionar'; aviso.onclick = () => { const w = document.getElementById('svc-select-wrap'); if(w) w.scrollIntoView({behavior:'smooth',block:'center'}); toggleSvcDropdown(); }; const navEl = cardData.querySelector('.calendar-nav'); if (navEl) cardData.insertBefore(aviso, navEl); else cardData.prepend(aviso); } // Opacidade reduzida no calendário const grid = document.getElementById('calendarGrid'); if (grid) grid.style.opacity = '0.35'; const nav = cardData.querySelector('.calendar-nav'); if (nav) nav.style.opacity = '0.35'; } else { // Serviço escolhido — remove bloqueios visuais if (trigger) { trigger.style.borderColor = ''; trigger.style.boxShadow = ''; } if (bloqEl) bloqEl.remove(); const grid = document.getElementById('calendarGrid'); if (grid) grid.style.opacity = ''; const nav = cardData.querySelector('.calendar-nav'); if (nav) nav.style.opacity = ''; } } // ✅ FIX: em mobile o navegador as vezes dispara o 'click' da opção mesmo quando // o dedo estava arrastando pra rolar a lista (o toque começa em cima de uma opção // e o clique "vaza" mesmo com o scroll acontecendo). Esse guard rastreia o // touchstart/touchmove no dropdown e cancela o clique se houve arrasto vertical // além de um pequeno limiar — assim o scroll funciona normalmente e só conta // como seleção quando é realmente um toque (tap) parado. let _cstTouchStartY = 0, _cstTouchStartX = 0, _cstTouchMoveu = false, _cstListenersOk = false; function _cstEnsureTouchGuard(dropdown) { if (_cstListenersOk || !dropdown) return; _cstListenersOk = true; dropdown.addEventListener('touchstart', e => { if (!e.touches || !e.touches[0]) return; _cstTouchStartY = e.touches[0].clientY; _cstTouchStartX = e.touches[0].clientX; _cstTouchMoveu = false; }, { passive: true }); dropdown.addEventListener('touchmove', e => { if (!e.touches || !e.touches[0]) return; const dy = Math.abs(e.touches[0].clientY - _cstTouchStartY); const dx = Math.abs(e.touches[0].clientX - _cstTouchStartX); if (dy > 8 || dx > 8) _cstTouchMoveu = true; }, { passive: true }); } function renderGridServicos(tiposConfig) { const dropdown = document.getElementById('svc-dropdown'); const trigger = document.getElementById('svc-trigger'); const selIdx = parseInt(document.getElementById('servico').value) || 0; const s = servicos[selIdx]; _cstEnsureTouchGuard(dropdown); if (s && _servicoEscolhido) { const tipo = s.tipo || 'Serviço'; const cor = _getTipoCor(tipo); const icone = _getTipoIcone(tipo); const imgHtml = s.imagem ? `` : `
${icone}
`; trigger.innerHTML = imgHtml + `
` + `
${s.nome}` + `${icone} ${tipo}` + `
` + `
${s.tempo} min${s.descricao ? ' · ' + s.descricao : ''}
` + `
` + `
R$ ${s.valor}
` + `
`; trigger.onclick = toggleSvcDropdown; trigger.style.borderColor = ''; trigger.style.boxShadow = ''; } else if (!_barbeiroEscolhido) { // Nenhum barbeiro escolhido — serviços dependem do barbeiro trigger.innerHTML = `
💈
` + `
` + `
Escolha o barbeiro primeiro
` + `
Cada barbeiro tem os seus próprios serviços
` + `
` + `
`; trigger.onclick = _pedirEscolhaBarbeiro; } else if (!_servicoEscolhido) { // Nenhum serviço escolhido ainda — exibe instrução chamativa trigger.innerHTML = `
✂️
` + `
` + `
👆 Toque aqui para escolher o serviço
` + `
Obrigatório antes de escolher a data
` + `
` + `
`; trigger.onclick = toggleSvcDropdown; } dropdown.innerHTML = ''; const tiposGrupos = {}; servicos.forEach((sv, i) => { const tipo = sv.tipo || 'Serviço'; if (!tiposGrupos[tipo]) tiposGrupos[tipo] = []; tiposGrupos[tipo].push({ sv, i }); }); // Ordena os tipos pela ordem configurada; tipos sem config vão ao final const _tcMap = {}; if (tiposConfig && tiposConfig.length) { tiposConfig.forEach((t, fi) => { _tcMap[t.nome] = t.ordem != null ? t.ordem : 9999 + fi; }); } const tiposOrdem = Object.keys(tiposGrupos).sort((a, b) => { const oa = _tcMap[a] != null ? _tcMap[a] : 9999; const ob = _tcMap[b] != null ? _tcMap[b] : 9999; return oa - ob; }); tiposOrdem.forEach(tipo => { const cor = _getTipoCor(tipo); const icone = _getTipoIcone(tipo); const header = document.createElement('div'); header.style.cssText = `padding:8px 14px 4px;font-size:0.62rem;font-weight:700;letter-spacing:1.5px;` + `text-transform:uppercase;color:${cor};background:${cor}18;` + `border-bottom:1px solid var(--border);`; header.textContent = icone + ' ' + tipo.toUpperCase(); dropdown.appendChild(header); tiposGrupos[tipo].forEach(({ sv, i }) => { const opt = document.createElement('div'); opt.className = 'cst-option' + (i === selIdx ? ' selected' : ''); const imgHtml = sv.imagem ? `` : `
${icone}
`; opt.innerHTML = imgHtml + `
` + `
${sv.nome}
` + `
${sv.tempo} min${sv.descricao ? ' · ' + sv.descricao : ''}
` + `
` + `
R$ ${sv.valor}
` + (i === selIdx ? '
' : ''); opt.onclick = () => { // ✅ FIX: se o toque foi um arrasto (scroll), ignora a "seleção" — // deixa o navegador rolar a lista normalmente. if (_cstTouchMoveu) { _cstTouchMoveu = false; return; } document.getElementById('servico').value = i; fecharSvcDropdown(); _servicoEscolhido = true; _atualizarBloqueioCalendario(); renderGridServicos(); onServicoChange(); }; dropdown.appendChild(opt); }); }); } function _pedirEscolhaBarbeiro(){ const grid = document.getElementById('grid-barbeiros'); if (!grid) return; grid.dataset.aberto = 'true'; renderGridBarbeiros(); grid.scrollIntoView({ behavior:'smooth', block:'center' }); grid.style.transition = 'box-shadow 0.2s'; grid.style.boxShadow = '0 0 0 2px var(--accent)'; setTimeout(() => { grid.style.boxShadow = ''; }, 1600); } function toggleSvcDropdown(){ if (!_barbeiroEscolhido) { _pedirEscolhaBarbeiro(); return; } const trigger=document.getElementById('svc-trigger'), dropdown=document.getElementById('svc-dropdown'); const aberto=dropdown.classList.contains('open'); if(aberto){fecharSvcDropdown();return;} // ✅ FIX: usa position:fixed com coords do trigger (getBoundingClientRect) // em vez de position:absolute — assim o dropdown fica sempre por cima do // card-data, independente do stacking context criado pelo backdrop-filter // dos cards (mesmo fix já usado no dropdown de serviço do admin/na-svc-dropdown). // ✅ FIX 2: backdrop-filter no ancestral (.card) cria um novo containing // block também para position:fixed — não só absolute. Isso fazia as // coordenadas (calculadas relativas à viewport) serem aplicadas relativas // ao card, deslocando o dropdown. Solução: mover o dropdown pro // enquanto está aberto, escapando de qualquer ancestral com backdrop-filter. const rect = trigger.getBoundingClientRect(); document.body.appendChild(dropdown); dropdown.style.position = 'fixed'; dropdown.style.top = (rect.bottom + 4) + 'px'; dropdown.style.left = rect.left + 'px'; dropdown.style.width = rect.width + 'px'; dropdown.style.right = 'auto'; trigger.classList.add('open');dropdown.classList.add('open'); document.getElementById('card-servico')?.classList.add('svc-dropdown-aberto'); } function fecharSvcDropdown(){ document.getElementById('svc-trigger').classList.remove('open'); const dropdown = document.getElementById('svc-dropdown'); dropdown.classList.remove('open'); const wrap = document.getElementById('svc-select-wrap'); if (wrap && dropdown.parentElement !== wrap) wrap.appendChild(dropdown); document.getElementById('card-servico')?.classList.remove('svc-dropdown-aberto'); } document.addEventListener('click',e=>{ const wrap=document.getElementById('svc-select-wrap'); const dropdown=document.getElementById('svc-dropdown'); const dentroWrap = wrap && wrap.contains(e.target); const dentroDropdown = dropdown && dropdown.contains(e.target); if(!dentroWrap && !dentroDropdown) fecharSvcDropdown(); }); // ✅ FIX: fecha o dropdown ao rolar a página, pra ele não ficar "grudado" // no lugar errado (já que agora é position:fixed, não acompanha o scroll). // IMPORTANTE: scroll não faz bubble, mas passa pela fase de CAPTURA em // window mesmo quando o scroll acontece dentro de um elemento filho (ex: a // própria lista de serviços). Sem o filtro abaixo, rolar a lista fechava o // dropdown na hora — por isso ignoramos eventos cujo alvo é o próprio dropdown. window.addEventListener('scroll', (e)=>{ const dropdown = document.getElementById('svc-dropdown'); if (dropdown && (e.target === dropdown || dropdown.contains(e.target))) return; fecharSvcDropdown(); }, true); window.addEventListener('resize', ()=>{ fecharSvcDropdown(); }); // [SUBSTITUÍDO] api.run → gsr.call const _cacheDatasBarb = {}; // { email: { ts, lista } } const _TTL_DATAS_BARB = 90 * 1000; // ✅ FIX: reduzido de 3min para 90s function recarregarDatasParaBarbeiro(emailBarbeiro) { const agora = Date.now(); // FIX: chave do cache inclui a duração do serviço selecionado // Sem isso, trocar de serviço mantinha datas erradas no cache const svc = servicos[document.getElementById('servico').value]; const duracaoSvc = svc ? Number(svc.tempo) : 0; const chaveCache = emailBarbeiro + '|dur' + duracaoSvc; const hit = _cacheDatasBarb[chaveCache]; // ✅ Cache válido → aplica direto, sem loading nem requisição if (hit && (agora - hit.ts) < _TTL_DATAS_BARB) { carregandoDatas = false; const lista = hit.lista; if (lista.length && typeof lista[0] === 'object') { datasDisponiveis = lista.map(r => r.data); lista.forEach(r => { vagasPorData[r.data] = r.vagas; }); } else { datasDisponiveis = lista; } renderCalendar(); return; } // 🔄 Cache expirado ou inexistente → busca no servidor com loading carregandoDatas = true; datasDisponiveis = []; vagasPorData = {}; document.getElementById('cal-loading-overlay').classList.add('visible'); renderCalendar(); // FIX: passa a duração do serviço para a API calcular vagas corretas gsr.call('getDatasDisponiveisBarbeiro', [emailBarbeiro, duracaoSvc || 0]) .then(resultado => { carregandoDatas = false; const lista = resultado || []; // Salva no cache com chave que inclui duração do serviço _cacheDatasBarb[chaveCache] = { ts: agora, lista }; // Mantém compatibilidade: persiste no localStorage só para o barbeiro padrão const b0 = barbeiros[0]; const chavePadrao = b0 ? ((b0.email && b0.email.trim()) ? b0.email.trim() : b0.nome) : ''; if (emailBarbeiro === chavePadrao && lista.length) { _cacheSalvar(CACHE.DATAS, lista); } if (lista.length && typeof lista[0] === 'object') { datasDisponiveis = lista.map(r => r.data); lista.forEach(r => { vagasPorData[r.data] = r.vagas; }); } else { datasDisponiveis = lista; } document.getElementById('cal-loading-overlay').classList.remove('visible'); renderCalendar(); }) .catch(() => { carregandoDatas = false; document.getElementById('cal-loading-overlay').classList.remove('visible'); renderCalendar(); }); } function onBarbeiroChange(){ renderGridBarbeiros(); atualizarResumo(); dataSelecionada=''; horarioSelecionado=''; // Reseta escolha de serviço ao trocar de barbeiro _servicoEscolhido = false; _atualizarBloqueioCalendario(); document.getElementById('card-horario').style.display='none'; document.getElementById('slotsWrap').innerHTML=''; document.getElementById('s3').className='step'; document.getElementById('line2').className='step-line'; const b=barbeiros[document.getElementById('barbeiro').value]; if(b){const chave=(b.email&&b.email.trim())?b.email.trim():b.nome;recarregarServicosParaBarbeiro(chave);recarregarDatasParaBarbeiro(chave);} } function onServicoChange() { renderGridServicos(); atualizarResumo(); // FIX: quando o serviço muda, as datas disponíveis podem mudar // (ex: serviço longo pode não caber em dias com poucos slots) // Recarrega o calendário com a duração do novo serviço const b = barbeiros[document.getElementById('barbeiro').value]; if (b) { const chave = (b.email && b.email.trim()) ? b.email.trim() : b.nome; // Invalida cache de datas (que agora inclui a duração do serviço na chave) Object.keys(_cacheDatasBarb).forEach(k => { if (k.startsWith(chave + '|dur')) delete _cacheDatasBarb[k]; }); recarregarDatasParaBarbeiro(chave); } if (!dataSelecionada) return; // FIX: delega para carregarSlots que agora usa chave com duração do serviço. // Se o cache para essa duração específica não existir, busca do servidor automaticamente. carregarSlots(dataSelecionada); } function mudarMes(dir){ viewMonth+=dir; if(viewMonth>11){viewMonth=0;viewYear++;} if(viewMonth<0){viewMonth=11;viewYear--;} renderCalendar(); } function renderCalendar(){ document.getElementById('mesAno').textContent=`${MESES[viewMonth]} ${viewYear}`; const btnPrev=document.getElementById('prevMonth'), btnNext=document.getElementById('nextMonth'); const hojeAno=hoje.getFullYear(), hojeMes=hoje.getMonth(); btnPrev.disabled=(viewYear{const h=document.createElement('div');h.className='cal-header';h.textContent=d;g.appendChild(h);}); const primeiroDiaSemana=new Date(viewYear,viewMonth,1).getDay(); const totalDias=new Date(viewYear,viewMonth+1,0).getDate(); const hoje0=new Date(hoje.getFullYear(),hoje.getMonth(),hoje.getDate()); for(let d=1;d<=totalDias;d++){ const dtAtual=new Date(viewYear,viewMonth,d); const ds=`${String(d).padStart(2,'0')}/${String(viewMonth+1).padStart(2,'0')}/${viewYear}`; const cell=document.createElement('div'); cell.className='cal-day'; if(d===1&&primeiroDiaSemana>0)cell.style.gridColumn=(primeiroDiaSemana+1).toString(); const inner=document.createElement('div'); inner.className='cal-day-inner'; inner.textContent=d; const isToday=dtAtual.getTime()===hoje0.getTime(); const isPast=dtAtualselecionarData(ds); if(vagasPorData[ds]){ const vagas=vagasPorData[ds]; const v=document.createElement('span');v.className='cal-vagas';v.textContent=vagas+'v';inner.appendChild(v); const maxV=Math.max(...Object.values(vagasPorData),1); const pct=Math.round((vagas/maxV)*100); const cor=vagas>=maxV*0.6?'#34d399':vagas>=maxV*0.3?'#fbbf24':'#f87171'; const wrap=document.createElement('div');wrap.className='cal-bar-wrap'; const fill=document.createElement('div');fill.className='cal-bar-fill';fill.style.width=pct+'%';fill.style.background=cor; wrap.appendChild(fill);inner.appendChild(wrap); } } else if(isAvailable){ cell.classList.add('available'); if(isToday)cell.classList.add('today'); cell.onclick=()=>selecionarData(ds); if(vagasPorData[ds]){ const vagas=vagasPorData[ds]; const v=document.createElement('span');v.className='cal-vagas';v.textContent=vagas+'v';inner.appendChild(v); const maxV=Math.max(...Object.values(vagasPorData),1); const pct=Math.round((vagas/maxV)*100); const cor=vagas>=maxV*0.6?'#34d399':vagas>=maxV*0.3?'#fbbf24':'#f87171'; const wrap=document.createElement('div');wrap.className='cal-bar-wrap'; const fill=document.createElement('div');fill.className='cal-bar-fill';fill.style.width=pct+'%';fill.style.background=cor; wrap.appendChild(fill);inner.appendChild(wrap); } } else if(isToday){cell.classList.add('today');} else{cell.classList.add('unavailable');} cell.appendChild(inner); g.appendChild(cell); } } function selecionarData(ds){ if (!_barbeiroEscolhido) { _pedirEscolhaBarbeiro(); return; } if (!_servicoEscolhido) { // Chama atenção para o serviço com animação const wrap = document.getElementById('svc-select-wrap'); if (wrap) { wrap.scrollIntoView({ behavior: 'smooth', block: 'center' }); wrap.style.transition = 'box-shadow 0.2s'; wrap.style.boxShadow = '0 0 0 2px var(--danger), 0 0 18px rgba(248,113,113,0.35)'; setTimeout(() => { wrap.style.boxShadow = ''; }, 1800); } // Abre o dropdown para facilitar toggleSvcDropdown(); return; } dataSelecionada=ds; horarioSelecionado=''; atualizarResumo(); renderCalendar(); carregarSlots(ds); } // [SUBSTITUÍDO] api.run → gsr.call (duas chamadas unificadas) const _cacheSlotsMap = {}; // { "ds|email": { ts, horarios } } const _TTL_SLOTS = 60 * 1000; // 60 segundos function carregarSlots(ds) { const card = document.getElementById('card-horario'); const wrap = document.getElementById('slotsWrap'); card.style.display = ''; const b = barbeiros[document.getElementById('barbeiro').value]; const emailBarbeiro = b ? (b.email || b.nome) : ''; // FIX: inclui duração do serviço na chave do cache — serviços diferentes podem ter // conjuntos de slots livres diferentes (ex: slot livre para 10min mas ocupado para 40min). const svcAtual = servicos[document.getElementById('servico').value]; const durSvcAtual = svcAtual ? Number(svcAtual.tempo) : 0; const chaveSlot = ds + '|' + emailBarbeiro + '|dur' + durSvcAtual; const agora = Date.now(); const hit = _cacheSlotsMap[chaveSlot]; // ✅ Cache válido → renderiza direto, sem requisição nem spinner if (hit && (agora - hit.ts) < _TTL_SLOTS) { montarSlots(hit.horarios); return; } // 🔄 Cache expirado ou inexistente → busca no servidor wrap.innerHTML = `
Buscando horários...
`; // FIX: passa duração do serviço para a API filtrar overlap corretamente. // Sem isso, a API usa durServico=bMin (ex: 40min) para qualquer serviço. // Sempre passa a duração real (nunca 0 ou string vazia — API interpreta 0 como bMin). const _durParaApi = durSvcAtual > 0 ? durSvcAtual : (_intervaloConfig || 30); const p = emailBarbeiro ? gsr.call('getHorariosPorBarbeiro', [ds, emailBarbeiro, _durParaApi]) : gsr.call('getHorarios', [ds, _durParaApi]); p.then(h => { _cacheSlotsMap[chaveSlot] = { ts: agora, horarios: h || [] }; montarSlots(h); }) .catch(() => { wrap.innerHTML = '

Erro ao carregar. Tente novamente.

'; }); } // Invalida caches de slots e datas de um barbeiro após agendamento confirmado // Invalida caches de slots e datas de um barbeiro após agendamento confirmado function _invalidarCachesBarbeiro(emailBarbeiro) { // Slots: chave "DS|email|durXXX" (nova) ou "DS|email" (legada) Object.keys(_cacheSlotsMap).forEach(k => { // Invalida tanto chaves novas (com |durXXX) quanto legadas (sem duração) if (k.includes('|' + emailBarbeiro + '|') || k.endsWith('|' + emailBarbeiro)) delete _cacheSlotsMap[k]; }); // BUG 3 FIX: chave real é "email|durXXX", não apenas "email" // O delete anterior nunca encontrava a chave e o cache ficava eternamente válido Object.keys(_cacheDatasBarb).forEach(k => { if (k.startsWith(emailBarbeiro + '|dur')) delete _cacheDatasBarb[k]; }); } function montarSlots(horarios){ const wrap=document.getElementById('slotsWrap'); wrap.innerHTML=''; if(!horarios||!horarios.length){wrap.innerHTML='

Nenhum horário disponível nesta data.

';return;} const[ds,ms,as]=dataSelecionada.split('/'); const dSel=new Date(as,ms-1,ds),tMid=new Date(hoje.getFullYear(),hoje.getMonth(),hoje.getDate()); const toM=h=>{const[a,b]=h.split(':').map(Number);return a*60+b;}; // ── FIX: extrai marcador __FIM__ injetado pela API como sentinela de fechamento ── // A API adiciona "__FIM__HH:MM" ao final de livresComFechamento quando o último slot // está ocupado, para que o frontend saiba o horário de fechamento mesmo sem ele nos livres. // Separar esse marcador antes da detecção de bMin evita que o gap entre o último slot // livre e o sentinela (que pode ser diferente de bMin) corrompa o cálculo de blocos. // // ── FIX PRINCIPAL: extrai __ALLSLOTS__ para distinção gap × slot ocupado ──────── // BUG anterior: o frontend bloqueava 15:30 porque 16:15 (endMin do serviço) não estava // em slotTimeSet — slotTimeSet só contém slots LIVRES. Se 16:15 está ocupado por outro // cliente, 16:15 não aparece, e 15:30 era incorretamente eliminado da lista. // CORREÇÃO: a API envia __ALLSLOTS__ com TODOS os slots configurados (livres + ocupados). // O frontend usa allSlotTimeSet para checar se endMin é um slot real ou um gap de almoço. let _fimHora = null; let _allSlotsSet = null; let _bMinServidor = null; let _horaFimReal = null; // NOVO: horário real de fechamento (cfg.hora_fim), não estimado const horariosSemFim = horarios.filter(h => { if(typeof h==='string' && h.startsWith('__FIM__')){ _fimHora = h.replace('__FIM__',''); return false; } if(typeof h==='string' && h.startsWith('__ALLSLOTS__')){ const ss = h.replace('__ALLSLOTS__','').split(',').filter(Boolean); _allSlotsSet = new Set(ss.map(s=>{const[a,b]=s.split(':').map(Number);return a*60+b;})); return false; } if(typeof h==='string' && h.startsWith('__BMIN__')){ const v = Number(h.replace('__BMIN__','')); if (!isNaN(v) && v > 0) _bMinServidor = v; return false; } if(typeof h==='string' && h.startsWith('__HORAFIM__')){ _horaFimReal = h.replace('__HORAFIM__',''); return false; } return true; }); // Horário de fechamento = marcador extraído OU último slot da lista sem marcador const ultimoSlotMin = _fimHora ? toM(_fimHora) : (horariosSemFim.length>0 ? toM(horariosSemFim[horariosSemFim.length-1]) : 0); // filt/slotTimeSet usam apenas slots reais (sem sentinela) let filt=horariosSemFim; if(dSel.getTime()===tMid.getTime()){const hh=hoje.getHours(),mm=hoje.getMinutes();filt=horariosSemFim.filter(h=>{const[a,b]=h.split(':').map(Number);return a>hh||(a===hh&&b>mm);});} const svc=servicos[document.getElementById('servico').value]; const tempo=svc?Number(svc.tempo):30; // ── FIX bMin: prioriza o valor REAL enviado pelo backend (__BMIN__). Só recorre // à heurística de adivinhação (gap entre os 3 primeiros slots) quando o backend // não mandou o sentinela (cache antigo de antes desse fix, ou versão desatualizada // do worker) — essa heurística falha quando a grade tem um trecho irregular // (ex: sub-slots de 15min dentro de uma grade de 30min), detectando bMin errado // e rejeitando slots válidos em cascata. let bMin=_intervaloConfig||30; if (_bMinServidor) { bMin = _bMinServidor; } else if(horariosSemFim.length>=3){ const g1=toM(horariosSemFim[1])-toM(horariosSemFim[0]); const g2=toM(horariosSemFim[2])-toM(horariosSemFim[1]); if(g1===g2&&g1>=15&&g1<=120)bMin=g1; } else if(horariosSemFim.length>=2){ // Com apenas 2 slots, só confia no gap se bate com _intervaloConfig (evita sentinela) const gap=toM(horariosSemFim[1])-toM(horariosSemFim[0]); if(gap>=15&&gap<=120&&gap===(_intervaloConfig||gap))bMin=gap; } const blocos=Math.ceil(tempo/bMin); // FIX: Detecta gaps no meio do dia (ex: pausa de almoço) // Constrói conjunto de todos os horários reais (sem sentinela) para checar se endMin cai num gap const slotTimeSet=new Set(horariosSemFim.map(h=>toM(h))); // FIX PRINCIPAL: usa _allSlotsSet (slots configurados, livres + ocupados) para detectar gaps. // Sem isso, um slot S ficava bloqueado quando endMin(S) coincidia com um slot OCUPADO, // pois slotTimeSet contém apenas livres. Ex: 15:30 era bloqueado porque 16:15 estava // ocupado por Joao H., mesmo que o serviço de 15:30 não conflitasse com ele. const checkEndSet = _allSlotsSet || slotTimeSet; // Horário real de fechamento = cfg.hora_fim enviado pela API (__HORAFIM__). // Só cai para a estimativa antiga (último slot + bMin) se o backend for // uma versão antiga que ainda não manda esse sentinela. const fechamentoReal = _horaFimReal ? toM(_horaFimReal) : (ultimoSlotMin + bMin); const validos=filt.filter((h,i)=>{ const endMin=toM(h)+tempo; // usa tempo real do serviço, não blocos*bMin // O serviço não pode terminar depois do horário real de fechamento if(endMin>fechamentoReal)return false; // NOTA: a verificação de "isso cai num gap de almoço" já foi feita no // BACKEND (getHorariosPorBarbeiro), slot a slot, comparando com o próximo // horário real configurado — de forma correta mesmo com grade irregular // (slots_json curado, não uniforme). Reaplicar essa checagem aqui no // frontend assumindo passos fixos de bMin (slotAnterior+bMin) rejeitava // por engano serviços cuja duração não é múltiplo exato do intervalo // (ex: serviço de 30/40min numa grade curada de 45min), porque o // horário de término caía "no meio" de um vão livre válido e não batia // com nenhum múltiplo de bMin — mesmo sem nenhum conflito real. if(blocos<=1)return true; // FIX: verifica blocos consecutivos usando allSlotTimeSet (todos os slots configurados, // livres + ocupados) para os slots INTERMEDIÁRIOS. // O serviço termina em endMin = início do próximo agendamento → não conflita. // Ex: 08:00+90min termina às 09:30 (ocupado) → OK, não conflita com quem começa às 09:30. // // Se _allSlotsSet não veio da API (cache antigo), reconstrói inferindo a grade completa: // percorre todos os slots esperados e aceita se existem em filt OU se o gap é consistente. const allSetCheck = _allSlotsSet || (() => { // Reconstrói grade completa: START_MIN até fechamentoReal em passos de bMin const s = new Set(); for(let t = toM(horariosSemFim[0] || '08:00'); t <= fechamentoReal; t += bMin) s.add(t); return s; })(); for(let j=1;j1){const av=document.createElement('p');av.style.cssText='font-size:0.78rem;color:var(--muted);margin-bottom:8px;grid-column:1/-1;';av.textContent='ℹ️ Este serviço ocupa '+blocos+' horários de '+bMin+' min.';wrap.appendChild(av);} validos.forEach(h=>{ const btn=document.createElement('div'); btn.className='slot'+(h===horarioSelecionado?' selected':''); // FIX: mostra horário de término real do serviço (usa tempo exato, não múltiplo de slot) const fim=toM(h)+tempo; const fimStr=String(Math.floor(fim/60)).padStart(2,'0')+':'+String(fim%60).padStart(2,'0'); btn.textContent=h; const sub=document.createElement('div');sub.style.cssText='font-size:0.68rem;opacity:0.7;margin-top:2px;';sub.textContent='até '+fimStr;btn.appendChild(sub); btn.onclick=()=>{horarioSelecionado=h;document.querySelectorAll('.slot').forEach(b=>b.classList.remove('selected'));btn.classList.add('selected');atualizarResumo();document.getElementById('s3').className='step done';document.getElementById('line2').className='step-line done';}; wrap.appendChild(btn); }); } function atualizarResumo(){ const b=barbeiros[document.getElementById('barbeiro').value],s=servicos[document.getElementById('servico').value]; document.getElementById('r-barbeiro').textContent=b?b.nome:'—'; document.getElementById('r-servico').textContent=s?s.nome:'—'; document.getElementById('r-data').textContent=dataSelecionada||'—'; document.getElementById('r-horario').textContent=horarioSelecionado||'—'; document.getElementById('r-duracao').textContent=s?s.tempo+' min':'—'; document.getElementById('r-valor').textContent=s?'R$ '+s.valor:'—'; } // ══════════════════════════════════════════════════════════════ // DADOS DO USUÁRIO // ══════════════════════════════════════════════════════════════ document.getElementById('email').addEventListener('input',function(){ if(clienteEmail)return; const b=document.getElementById('bloco-cadastro'); b.style.display=this.value.trim().length>3?'block':'none'; if(this.value.trim().length<=3)toggleCadastro(false); }); function toggleCadastro(sim){ querCadastrar=sim; document.getElementById('opc-sim').classList.toggle('active',sim); document.getElementById('opc-nao').classList.toggle('active',!sim); document.getElementById('campos-senha').style.display=sim?'flex':'none'; if(!sim){document.getElementById('senha').value='';document.getElementById('confirma').value='';} } // [SUBSTITUÍDO] api.run → gsr.call function _carregarEPreencherDados(email){ gsr.call('buscarDadosCliente', [email]) .then(d => { clienteNome=d.nome||''; clienteTelefone=d.telefone||''; _aplicarDadosLogado(); if(clienteNome)document.getElementById('cli-nome-display').textContent=clienteNome; // Agora que temos o telefone, carrega o card BarberVip+ if(clienteTelefone) _carregarBcCliente(); }) .catch(() => { _aplicarDadosLogado(); }); } function _aplicarDadosLogado(){ const conteudoVisivel=document.getElementById('agendar-content').style.display!=='none'; if(!conteudoVisivel||!clienteEmail)return; const banner=document.getElementById('banner-logado'); banner.style.display='flex'; document.getElementById('banner-logado-nome').textContent=clienteNome?'Olá, '+clienteNome+'!':'Bem-vindo de volta!'; document.getElementById('banner-logado-sub').textContent=clienteNome?clienteEmail:'Dados preenchidos automaticamente'; const campoNome=document.getElementById('nome'),campoTel=document.getElementById('telefone'),campoEmail=document.getElementById('email'); if(clienteNome&&!campoNome.value.trim())campoNome.value=clienteNome; if(clienteTelefone&&!campoTel.value.trim())campoTel.value=clienteTelefone; if(clienteEmail&&!campoEmail.value.trim())campoEmail.value=clienteEmail; campoEmail.style.display='none'; document.getElementById('bloco-cadastro').style.display='none'; } // ══════════════════════════════════════════════════════════════ // AGENDAMENTO // ══════════════════════════════════════════════════════════════ let _agendaLinkGCal=null,_agendaICSBase64=null; // ── Helpers para montar Google Calendar / .ics no CLIENTE ────────────────── // (o backend nunca gerava res.linkGoogleCalendar / res.icsBase64 — por isso // o botão "Adicionar à Agenda" nunca aparecia funcional. Geramos tudo aqui.) // OBS: evitamos barra invertida literal no código porque este arquivo é // reembutido em outro template literal no deploy, e um nivel de escaping // e consumido nesse processo, quebrando regex/strings que dependam dela. var _BS = String.fromCharCode(92); // barra invertida var _LF = String.fromCharCode(10); // quebra de linha (LF) var _CR = String.fromCharCode(13); // retorno de carro (CR) function _parseDataHoraBR(dataStr, horaStr) { const [d, m, y] = String(dataStr || '').split('/').map(Number); const [hh, mm] = String(horaStr || '00:00').split(':').map(Number); return new Date(y || new Date().getFullYear(), (m || 1) - 1, d || 1, hh || 0, mm || 0, 0, 0); } function _formatICSDate(date) { const p = n => String(n).padStart(2, '0'); return date.getUTCFullYear() + p(date.getUTCMonth() + 1) + p(date.getUTCDate()) + 'T' + p(date.getUTCHours()) + p(date.getUTCMinutes()) + p(date.getUTCSeconds()) + 'Z'; } function _escapeICS(str) { return String(str || '') .split(_BS).join(_BS + _BS) .split(';').join(_BS + ';') .split(',').join(_BS + ',') .split(_LF).join(_BS + 'n'); } function _gerarLinksAgenda(nomeServico, nomeBarbeiro, dataStr, horaStr, duracaoMin) { const inicio = _parseDataHoraBR(dataStr, horaStr); const fim = new Date(inicio.getTime() + (Number(duracaoMin) || 30) * 60000); var nomeLoja = document.title; var _pos = nomeLoja.indexOf(' — '); if (_pos === -1) _pos = nomeLoja.indexOf(' - '); if (_pos !== -1) nomeLoja = nomeLoja.substring(0, _pos); nomeLoja = nomeLoja.trim() || 'Barbearia'; const titulo = nomeServico ? (nomeServico + ' — ' + nomeLoja) : nomeLoja; const detalhes = 'Serviço: ' + (nomeServico || '-') + _LF + 'Barbeiro: ' + (nomeBarbeiro || '-') + _LF + 'Agendado via BarberOS+'; const linkGoogleCalendar = 'https://calendar.google.com/calendar/render?action=TEMPLATE' + '&text=' + encodeURIComponent(titulo) + '&dates=' + _formatICSDate(inicio) + '/' + _formatICSDate(fim) + '&details=' + encodeURIComponent(detalhes) + '&location=' + encodeURIComponent(nomeLoja); const uid = 'barberos-' + Date.now() + '-' + Math.random().toString(36).slice(2) + '@barberos'; const icsStr = [ 'BEGIN:VCALENDAR', 'VERSION:2.0', 'PRODID:-//BarberOS+//Agendamento//PT-BR', 'CALSCALE:GREGORIAN', 'BEGIN:VEVENT', 'UID:' + uid, 'DTSTAMP:' + _formatICSDate(new Date()), 'DTSTART:' + _formatICSDate(inicio), 'DTEND:' + _formatICSDate(fim), 'SUMMARY:' + _escapeICS(titulo), 'DESCRIPTION:' + _escapeICS(detalhes), 'LOCATION:' + _escapeICS(nomeLoja), 'BEGIN:VALARM', 'TRIGGER:-PT1H', 'ACTION:DISPLAY', 'DESCRIPTION:Lembrete de agendamento', 'END:VALARM', 'END:VEVENT', 'END:VCALENDAR' ].join(_CR + _LF); let icsBase64 = ''; try { icsBase64 = btoa(unescape(encodeURIComponent(icsStr))); } catch (e) { try { icsBase64 = btoa(icsStr); } catch (e2) { icsBase64 = ''; } } return { linkGoogleCalendar, icsBase64 }; } // ── Lembrete via notificação do navegador, 1h antes (best-effort) ───────── // Só dispara enquanto esta aba/site ficar aberta — navegador fechado não // recebe. Para funcionar com o site fechado seria necessário Push real // (Service Worker + VAPID + agendamento no servidor). // Retorna: 'agendado' | 'pendente' (aguardando permissão) | 'passou' | 'muito_longe' | 'negado' | 'erro' function _agendarLembreteNavegador(nomeServico, nomeBarbeiro, dataStr, horaStr) { try { if (!('Notification' in window)) return 'erro'; const inicio = _parseDataHoraBR(dataStr, horaStr); const msAlvo = inicio.getTime() - 60 * 60 * 1000; // 1h antes const msEspera = msAlvo - Date.now(); const MAX_TIMEOUT = 2147000000; // limite prático do setTimeout (~24.8 dias) if (msEspera <= 0) return 'passou'; if (msEspera > MAX_TIMEOUT) return 'muito_longe'; const disparar = () => { try { new Notification('⏰ Seu horário é daqui a 1 hora!', { body: (nomeServico || 'Agendamento') + (nomeBarbeiro ? (' com ' + nomeBarbeiro) : '') + ' às ' + horaStr, }); } catch (e) {} }; const programar = () => setTimeout(disparar, msEspera); if (Notification.permission === 'granted') { programar(); return 'agendado'; } if (Notification.permission === 'denied') return 'negado'; Notification.requestPermission().then(p => { if (p === 'granted') programar(); }); return 'pendente'; } catch (e) { return 'erro'; } } function fecharModalAgenda(){document.getElementById('agendaModal').classList.remove('open');} let _agendaDadosAtual = null; function abrirModalAgenda(res,dadosAgen){ document.getElementById('am-servico').textContent=dadosAgen.servico||'—'; document.getElementById('am-barbeiro').textContent=dadosAgen.barbeiro||'—'; document.getElementById('am-data').textContent=dadosAgen.data||'—'; document.getElementById('am-horario').textContent=dadosAgen.horario||'—'; const btnGCal=document.getElementById('btn-google-cal'); if(res.linkGoogleCalendar){btnGCal.href=res.linkGoogleCalendar;btnGCal.style.display='flex';}else{btnGCal.style.display='none';} const btnICS=document.getElementById('btn-ics-cal'); if(res.icsBase64){btnICS.href='data:text/calendar;base64,'+res.icsBase64;btnICS.style.display='flex';}else{btnICS.style.display='none';} _agendaDadosAtual = dadosAgen; const tituloLembrete = document.getElementById('btn-lembrete-nav-titulo'); const subLembrete = document.getElementById('btn-lembrete-nav-sub'); if (tituloLembrete) tituloLembrete.textContent = 'Avisar no navegador'; if (subLembrete) subLembrete.textContent = 'Alerta pop-up 1h antes (com a aba aberta)'; document.getElementById('agendaModal').classList.add('open'); } function ativarLembreteNavegador(){ const tituloEl = document.getElementById('btn-lembrete-nav-titulo'); const subEl = document.getElementById('btn-lembrete-nav-sub'); if (!_agendaDadosAtual || !_agendaDadosAtual.data || !_agendaDadosAtual.horario) { if (subEl) subEl.textContent = 'Não foi possível identificar data/horário.'; return; } if (!('Notification' in window)) { if (subEl) subEl.textContent = 'Seu navegador não suporta essa notificação.'; return; } if (Notification.permission === 'denied') { if (subEl) subEl.textContent = 'Permissão bloqueada — habilite notificações nas configurações do navegador.'; return; } const resultado = _agendarLembreteNavegador(_agendaDadosAtual.servico, _agendaDadosAtual.barbeiro, _agendaDadosAtual.data, _agendaDadosAtual.horario); if (resultado === 'agendado') { if (tituloEl) tituloEl.textContent = '✅ Lembrete ativado'; if (subEl) subEl.textContent = 'Vamos te avisar 1h antes, com esta aba aberta.'; } else if (resultado === 'pendente') { if (subEl) subEl.textContent = 'Aguardando permissão do navegador...'; } else if (resultado === 'passou') { if (subEl) subEl.textContent = 'Já falta menos de 1h — sem tempo para avisar antes.'; } else if (resultado === 'muito_longe') { if (subEl) subEl.textContent = 'Data muito distante — volte aqui mais perto do dia.'; } else if (resultado === 'negado') { if (subEl) subEl.textContent = 'Permissão negada.'; } else { if (subEl) subEl.textContent = 'Não foi possível ativar o lembrete.'; } } // [SUBSTITUÍDO] api.run → gsr.call function agendar(){ const nome=document.getElementById('nome').value.trim(); const tel=document.getElementById('telefone').value.trim(); const email=clienteEmail||document.getElementById('email').value.trim(); const obsCliente=(document.getElementById('obs-cliente')?.value||'').trim(); const senha=document.getElementById('senha')?document.getElementById('senha').value:''; const conf=document.getElementById('confirma')?document.getElementById('confirma').value:''; const svc=servicos[document.getElementById('servico').value]; const barb=barbeiros[document.getElementById('barbeiro').value]; if(!_servicoEscolhido||!svc){SW('Escolha um serviço','Toque no campo de serviço e selecione o serviço desejado antes de continuar.','warning');return;} if(!nome||!tel){SW('Preencha nome e telefone','','warning');return;} if(!validarTel(tel)){SW('Telefone invalido','Informe um telefone com DDD. Ex: (85) 99999-9999','warning');return;} if(!dataSelecionada){SW('Selecione uma data','','warning');return;} if(!horarioSelecionado){SW('Selecione um horario','','warning');return;} if(!clienteEmail&&querCadastrar){ if(!email){SW('Informe o e-mail','O e-mail e necessario para criar sua conta.','warning');return;} if(senha.length<6){SW('Senha muito curta','Minimo 6 caracteres.','warning');return;} if(senha!==conf){SW('As senhas nao coincidem','','warning');return;} } const bMin=_intervaloConfig||30,hOcup=[]; let[h,m]=horarioSelecionado.split(':').map(Number); // FIX: marca todos os slots da grade que o serviço ocupa (startMin até startMin+tempo, passo bMin) {const startMin=h*60+m,endMin=startMin+Number(svc.tempo);for(let t=startMin;t { Swal.fire({ icon: 'warning', title: '⏰ Horário não disponível', html: '
' + 'Este horário foi ocupado por outro agendamento
' + 'enquanto você finalizava o formulário.

' + 'Por favor, escolha outro horário.' + '
', background: '#0e1220', color: '#e8edf8', confirmButtonColor: '#4f8ef7', confirmButtonText: 'Escolher outro horário', }); horarioSelecionado = ''; document.querySelectorAll('.slot').forEach(s => s.classList.remove('selected')); document.getElementById('s3').className = 'step'; document.getElementById('line2').className = 'step-line'; const _chInv = (barb.email && barb.email.trim()) ? barb.email.trim() : barb.nome; _invalidarCachesBarbeiro(_chInv); recarregarDatasParaBarbeiro(_chInv); if (dataSelecionada) carregarSlots(dataSelecionada); atualizarResumo(); }; _pSlotsVal.then(_slotsAtuais => { // ── Extrai sentinelas da resposta fresca da API ────────────────────────────── // __ALLSLOTS__ = todos os slots configurados (livres + ocupados), usado para // derivar o bMin REAL da grade, independente de _intervaloConfig. // __FIM__ = horário de fechamento (não usado aqui, só filtrado). let _valAllSlots = null; const _livresArr = (_slotsAtuais || []).filter(s => { if (typeof s !== 'string') return false; if (s.startsWith('__ALLSLOTS__')) { _valAllSlots = s.replace('__ALLSLOTS__', '').split(',').filter(Boolean); return false; } if (s.startsWith('__FIM__')) return false; return true; }); const _livres = new Set(_livresArr); // ── Deriva bMin REAL a partir do __ALLSLOTS__ retornado pela API ───────────── // Isso evita usar _intervaloConfig (que pode estar desatualizado ou errado) // e garante que hOcup seja reconstruído com o passo correto da grade. let _bMinReal = _intervaloConfig || 30; if (_valAllSlots && _valAllSlots.length >= 2) { const _toM = s => { const [a,b] = s.split(':').map(Number); return a*60+b; }; const _gap = _toM(_valAllSlots[1]) - _toM(_valAllSlots[0]); if (_gap >= 10 && _gap <= 120) _bMinReal = _gap; } // ── Reconstrói hOcup com o bMin REAL para enviar ao servidor ─────────────── // O hOcup original (linha 4675) usa _intervaloConfig que pode diferir do bMin // real da grade (ex: _intervaloConfig=30 mas slots são de 40min → hOcup errado). const _hh = Number(horarioSelecionado.split(':')[0]); const _mm = Number(horarioSelecionado.split(':')[1]); const _startMin = _hh * 60 + _mm; const _endMin = _startMin + Number(svc.tempo); const _hOcupReal = []; for (let t = _startMin; t < _endMin; t += _bMinReal) { _hOcupReal.push(String(Math.floor(t/60)).padStart(2,'0') + ':' + String(t%60).padStart(2,'0')); } // Atualiza hOcup para o servidor usar o passo correto da grade hOcup.length = 0; _hOcupReal.forEach(s => hOcup.push(s)); // ── Verifica apenas se o horário INICIAL está nos slots livres ─────────────── // _livres contém slots onde a API aprovou aquele horário como START de um serviço // de duração svc.tempo. Os slots intermediários (hOcupReal[1], [2]...) NÃO devem // ser checados em _livres: a API os exclui de _livres porque como START gerariam // overlap com o próximo agendamento — mas como slots intermediários do serviço // atual eles são válidos (o serviço já termina antes do próximo). // Exemplo: 08:40+80min=10:00 (OK). "09:20" não está em _livres (09:20+80=10:40 // colide com Patrick), mas isso não importa para quem está agendando às 08:40. if (!_livres.has(horarioSelecionado)) { _mostrarHorarioOcupado(); return; } // ── Slots confirmados livres → envia para o servidor ───────────────────── SWL('Confirmando...'); gsr.call('salvarAgendamentoVerificando', [{nome,telefone:tel,email,servico:svc.nome,barbeiro:barb.nome,barbeiro_email:barb.email||'',data:dataSelecionada,horarios:hOcup,forcar:false,cadastrar:!clienteEmail&&querCadastrar,senha:(!clienteEmail&&querCadastrar)?senha:'',obs:obsCliente}]) .then(res => { Swal.close(); if (!res || res.conflito) { _mostrarHorarioOcupado(); return; } mostrarWhatsAppFab(); // ── Gera Google Calendar / .ics e guarda um snapshot dos dados AGORA, // ── enquanto svc/barb/dataSelecionada/horarioSelecionado ainda têm os // ── valores certos (são zerados mais abaixo, antes do popup ser fechado) ── let _dadosParaModalSnapshot = { servico: svc.nome, barbeiro: barb ? barb.nome : '', data: dataSelecionada, horario: horarioSelecionado }; try { const _linksAgenda = _gerarLinksAgenda(svc.nome, barb ? barb.nome : '', dataSelecionada, horarioSelecionado, svc.tempo); _agendaLinkGCal = _linksAgenda.linkGoogleCalendar; _agendaICSBase64 = _linksAgenda.icsBase64; } catch (eAgenda) { _agendaLinkGCal = null; _agendaICSBase64 = null; } // ── Monta mensagem WhatsApp com os dados do agendamento ────────── var _LF = String.fromCharCode(10); var _nomeConf = (document.getElementById('nome') ? document.getElementById('nome').value.trim() : '') || clienteNome || ''; var _servicoConf = document.getElementById('r-servico') ? document.getElementById('r-servico').textContent : ''; var _barbeiroConf = document.getElementById('r-barbeiro') ? document.getElementById('r-barbeiro').textContent : ''; var _dataConf = document.getElementById('r-data') ? document.getElementById('r-data').textContent : ''; var _horarioConf = document.getElementById('r-horario') ? document.getElementById('r-horario').textContent : ''; var _emailConf = clienteEmail || (document.getElementById('email') ? document.getElementById('email').value.trim() : '') || ''; var _telConf = (document.getElementById('telefone') ? document.getElementById('telefone').value.trim() : '') || clienteTelefone || ''; var _msgWa = 'Agendamento Confirmado!' + _LF + _LF + 'Cliente: ' + _nomeConf + _LF + 'Servico: ' + _servicoConf + _LF + 'Barbeiro: ' + _barbeiroConf + _LF + 'Data: ' + _dataConf + _LF + 'Horario: ' + _horarioConf + (_telConf ? _LF + 'Telefone: ' + _telConf : '') + _LF + _LF + 'Agendado via BarberOS+'; var _whatsNumConf = WHATS_BARBEARIA || WHATS_LOJA; var _whatsUrl = _whatsNumConf ? 'https://wa.me/' + _whatsNumConf + '?text=' + encodeURIComponent(_msgWa) : 'https://wa.me/?text=' + encodeURIComponent(_msgWa); // ── HTML dos botões de confirmação ──────────────────────────────── let _botoesHtml = '
' + (res.cadastroRealizado ? 'Conta criada! Acesse Meus Agendamentos com seu e-mail e senha.' : res.cadastroJaExiste ? 'E-mail ja cadastrado — agendamento vinculado à sua conta.' : _emailConf ? 'Confirmação enviada para ' + _emailConf + '.' : 'Seu horário foi confirmado com sucesso.') + '
' + '
'; if (res.agendamento_id && _mpConectadoGlobal) { var _valorSvc = Number(svc.valor || 0); _botoesHtml += ''; } if (_whatsUrl) { _botoesHtml += '' + '' + 'Confirmar pelo WhatsApp'; } if (res.agendamento_id && 'serviceWorker' in navigator && 'PushManager' in window) { var _pushEmail = (_emailConf || '').replace(/["'<>]/g, ''); var _pushTel = (_telConf || '').replace(/["'<>]/g, ''); // Não ativa mais sozinho (isso dava timeout/erro em vários // navegadores, já que pedir permissão de notificação sem um clique // direto do usuário é bloqueado/atrasado por eles). Em vez disso, // só chama atenção com um aviso pra lembrar de clicar. _botoesHtml += '
' + '👇 Não esqueça de ativar o aviso abaixo pra não perder o horário!
'; _botoesHtml += ''; } _botoesHtml += '
'; Swal.fire({ icon: 'success', title: 'Agendamento realizado!', html: _botoesHtml, background: '#0e1220', color: '#e8edf8', confirmButtonColor: '#4f8ef7', showCancelButton: true, confirmButtonText: 'Adicionar à Agenda', cancelButtonText: 'Fechar', cancelButtonColor: '#1e2a45', }).then(function(r) { if (r.isConfirmed && (_agendaLinkGCal || _agendaICSBase64)) { abrirModalAgenda({ linkGoogleCalendar: _agendaLinkGCal, icsBase64: _agendaICSBase64 }, _dadosParaModalSnapshot); } const emailUsado = clienteEmail || document.getElementById('email').value.trim(); if (res.cadastroRealizado || res.cadastroJaExiste) { clienteEmail = emailUsado; entrarAreaCli(emailUsado); mostrarSecao('cliente'); } else if (clienteEmail) { carregarAgenCli(); mostrarSecao('cliente'); } }); horarioSelecionado = ''; dataSelecionada = ''; document.getElementById('slotsWrap').innerHTML = ''; document.getElementById('card-horario').style.display = 'none'; document.getElementById('s3').className = 'step'; document.getElementById('line2').className = 'step-line'; const bAtual = barbeiros[document.getElementById('barbeiro').value]; if (bAtual) { _invalidarCachesBarbeiro(bAtual.email || bAtual.nome); // agendamento confirmado → dados desatualizados recarregarDatasParaBarbeiro(bAtual.email || bAtual.nome); } else renderCalendar(); atualizarResumo(); if (!clienteEmail) { ['nome','telefone','email','senha','confirma'].forEach(function(id){ const el = document.getElementById(id); if (el) el.value = ''; }); document.getElementById('bloco-cadastro').style.display = 'none'; toggleCadastro(false); } else { document.getElementById('telefone').value = clienteTelefone || ''; document.getElementById('nome').value = clienteNome || ''; } const obsEl = document.getElementById('obs-cliente'); if (obsEl) obsEl.value = ''; }) .catch(err => { Swal.close(); SW('Erro',err.message||'Falha ao agendar.','error'); }); }).catch(err => { Swal.close(); SW('Erro ao verificar horário',err.message||'Tente novamente.','error'); }); } // ══════════════════════════════════════════════════════════════ // CLIENTE // ══════════════════════════════════════════════════════════════ ['cli-email','cli-senha'].forEach(id=>document.getElementById(id).addEventListener('keydown',e=>{if(e.key==='Enter')clienteLogin();})); ['cad-nome','cad-telefone','cad-email','cad-senha','cad-confirma'].forEach(id=>{const el=document.getElementById(id);if(el)el.addEventListener('keydown',e=>{if(e.key==='Enter')clienteCadastrar();});}); function _setBtnLoading(id,loading,orig){ const btn=document.getElementById(id);if(!btn)return; if(loading){btn.disabled=true;btn.dataset.orig=btn.textContent;btn.innerHTML=' Aguarde...';} else{btn.disabled=false;btn.textContent=btn.dataset.orig||orig||'Enviar';} } // [SUBSTITUÍDO] api.run → gsr.call function clienteLogin(){ const email=document.getElementById('cli-email').value.trim(); const senha=document.getElementById('cli-senha').value; const err=document.getElementById('cli-err');err.style.display='none'; if(!email||!senha){err.textContent='Preencha e-mail e senha.';err.style.display='block';return;} _setBtnLoading('btn-login',true); gsr.call('validarLoginCliente', [email, senha]) .then(ok => { _setBtnLoading('btn-login',false,'Entrar'); if(ok && ok.valido){ clienteEmail=email; entrarAreaCli(email); } else{err.textContent='E-mail ou senha incorretos.';err.style.display='block';} }) .catch(() => { _setBtnLoading('btn-login',false,'Entrar'); SW('Erro','Falha na conexão.','error'); }); } // [SUBSTITUÍDO] api.run → gsr.call function clienteCadastrar(){ const nome=document.getElementById('cad-nome').value.trim(); const telefone=document.getElementById('cad-telefone').value.trim(); const email=document.getElementById('cad-email').value.trim(); const senha=document.getElementById('cad-senha').value; const conf=document.getElementById('cad-confirma').value; const err=document.getElementById('cad-err');err.style.display='none'; if(!nome){err.textContent='Informe seu nome completo.';err.style.display='block';return;} if(!telefone){err.textContent='Informe seu telefone.';err.style.display='block';return;} if(!validarTel(telefone)){err.textContent='Telefone inválido. Use o formato (XX) XXXXX-XXXX.';err.style.display='block';return;} if(!email||!senha){err.textContent='Preencha todos os campos.';err.style.display='block';return;} if(senha.length<6){err.textContent='Senha deve ter mínimo 6 caracteres.';err.style.display='block';return;} if(senha!==conf){err.textContent='As senhas não coincidem.';err.style.display='block';return;} _setBtnLoading('btn-cadastrar',true); gsr.call('cadastrarUsuario', [email, senha, nome, telefone]) .then(msg => { _setBtnLoading('btn-cadastrar',false,'Criar Conta'); if(msg.includes('sucesso')){clienteEmail=email;clienteNome=nome;clienteTelefone=telefone;entrarAreaCli(email);} else{err.textContent=msg;err.style.display='block';} }) .catch(() => { _setBtnLoading('btn-cadastrar',false,'Criar Conta'); SW('Erro','Falha ao criar conta.','error'); }); } // [SUBSTITUÍDO] api.run → gsr.call function entrarAreaCli(email){ clienteEmail=email;sessaoSalvar(email);mostrarSubCli('area'); const nomeTemp=email.split('@')[0].replace(/[._]/g,' ').replace(/\b\w/g,c=>c.toUpperCase()); document.getElementById('cli-nome-display').textContent=clienteNome||nomeTemp; document.getElementById('cli-email-display').textContent=email; if(!clienteNome){_carregarEPreencherDados(email);}else{_aplicarDadosLogado();} carregarAgenCli(); _carregarBcCliente(); gsr.call('getTemaBarbearia') .then(data => { aplicarTema(data?.tema || 'padrao'); }) .catch(() => {}); } // ── BarberVip+ no index do cliente ──────────────────────────────── function _carregarBcCliente() { const tel = clienteTelefone ? clienteTelefone.replace(/D/g,'') : ''; // Sempre tenta buscar — passa email como fallback quando não tem telefone // Se ainda não buscou os dados do cliente, busca antes para ter o telefone if (!tel && clienteEmail) { gsr.call('buscarDadosCliente', [clienteEmail]) .then(function(d) { if (d && d.telefone) clienteTelefone = d.telefone; // Chama de novo — agora com telefone (ou só com email se não tiver) const telAtual = clienteTelefone ? clienteTelefone.replace(/D/g,'') : ''; gsr.call('getBarberCoinPontos', [telAtual, '', clienteEmail]) .then(function(res) { _renderBcCard(res); }) .catch(function() { const c=document.getElementById('cli-bc-card'); if(c)c.style.display='none'; }); }).catch(function(){}); return; } gsr.call('getBarberCoinPontos', [tel, '', clienteEmail || '']) .then(function(res) { _renderBcCard(res); }) .catch(function(){ const card = document.getElementById('cli-bc-card'); if (card) card.style.display = 'none'; }); } function mascaraTel(input) { let v = input.value.replace(/D/g, '').slice(0, 11); if (v.length <= 10) { v = v.replace(/^(d{2})(d{4})(d{0,4})/, '($1) $2-$3'); } else { v = v.replace(/^(d{2})(d{5})(d{0,4})/, '($1) $2-$3'); } input.value = v; } function _renderBcCard(res) { const card = document.getElementById('cli-bc-card'); if (!card) return; if (!res || !res.programaAtivo || !res.encontrado) { card.style.display='none'; return; } const pontos = res.saldo || 0; const meta = res.meta || 10; const pronto = pontos >= meta; const isLivre = (res.modo || 'cartela') === 'livre'; // Badge const badge = document.getElementById('cli-bc-badge'); if (badge) badge.textContent = isLivre ? pontos + ' 🪙' : pontos + ' / ' + meta; // Barra de progresso const bar = document.getElementById('cli-bc-bar'); if (bar) { bar.style.width = Math.min(100, Math.round((pontos/meta)*100)) + '%'; bar.style.background = pronto ? '#f5a623' : 'var(--accent)'; } // Stamps (cartela) ou texto livre const stampsEl = document.getElementById('cli-bc-stamps'); if (stampsEl) { if (isLivre) { stampsEl.innerHTML = 'Acumule ' + meta + ' 🪙 para ganhar o prêmio'; } else { let html = ''; for (let i = 0; i < meta; i++) { const cheio = i < pontos; html += '
'+(cheio?'🪙':'')+'
'; } stampsEl.innerHTML = html; } } // Prêmio conquistado const premioWrap = document.getElementById('cli-bc-premio-wrap'); const premioNome = document.getElementById('cli-bc-premio-nome'); if (premioWrap) premioWrap.style.display = pronto ? 'block' : 'none'; if (premioNome && res.premio) premioNome.textContent = res.premio; card.style.display = 'block'; } function validarTel(tel){const n=tel.replace(/D/g,'');return n.length===10||n.length===11;} function clienteLogout(){ clienteEmail='';clienteNome='';clienteTelefone='';agendamentosCliente=[];sessaoLimpar(); ['nome','telefone'].forEach(id=>{const el=document.getElementById(id);if(el)el.value='';}); document.getElementById('email').style.display='';document.getElementById('email').value=''; document.getElementById('banner-logado').style.display='none'; document.getElementById('bloco-cadastro').style.display='none'; document.getElementById('cli-email').value='';document.getElementById('cli-senha').value=''; document.getElementById('cli-err').style.display='none'; mostrarSubCli('login'); Swal.fire({toast:true,position:'top-end',icon:'info',title:'Sessão encerrada',showConfirmButton:false,timer:2200,background:'#0e1220',color:'#e8edf8'}); } function toggleSenha(){ const f=document.getElementById('cli-form-senha'); // Fecha o formulário de perfil se estiver aberto const fp=document.getElementById('cli-form-perfil'); if(fp && fp.style.display!=='none') togglePerfil(); f.style.display=f.style.display==='none'?'block':'none'; if(f.style.display==='none'){['s-atual','s-nova','s-confirma'].forEach(id=>document.getElementById(id).value='');document.getElementById('s-err').style.display='none';} } // Exibe/oculta campo de senha — usado nos formulários de login e cadastro function perfilToggleSenha(id, btn) { const input = document.getElementById(id); if (!input) return; const mostrar = input.type === 'password'; input.type = mostrar ? 'text' : 'password'; btn.textContent = mostrar ? '🙈' : '👁'; } // [SUBSTITUÍDO] api.run → gsr.call function clienteAlterarSenha(){ const atual=document.getElementById('s-atual').value; const nova=document.getElementById('s-nova').value; const conf=document.getElementById('s-confirma').value; const err=document.getElementById('s-err');err.style.display='none'; if(!atual||!nova){err.textContent='Preencha todos os campos.';err.style.display='block';return;} if(nova.length<6){err.textContent='Nova senha deve ter mínimo 6 caracteres.';err.style.display='block';return;} if(nova!==conf){err.textContent='As senhas não coincidem.';err.style.display='block';return;} SWL('Alterando...'); gsr.call('alterarSenhaCliente', [clienteEmail, atual, nova]) .then(ok => { Swal.close(); if(ok){SW('Senha alterada!','','success');toggleSenha();} else{err.textContent='Senha atual incorreta.';err.style.display='block';} }) .catch(() => { Swal.close(); SW('Erro','','error'); }); } // ── Painel de "Notificações e avisos" na Conta do cliente ──────────── function toggleNotifCli(){ const f=document.getElementById('cli-form-notif'); // Fecha os outros formulários se estiverem abertos const fp=document.getElementById('cli-form-perfil'); if(fp && fp.style.display!=='none') togglePerfil(); const fs=document.getElementById('cli-form-senha'); if(fs && fs.style.display!=='none') toggleSenha(); const abrindo=f.style.display==='none'; f.style.display=abrindo?'block':'none'; if(abrindo) _atualizarStatusNotifCli(); } function _atualizarStatusNotifCli(){ const el=document.getElementById('cli-notif-status'); const btn=document.getElementById('btn-ativar-notif-todos'); if(!el) return; const suportado=('serviceWorker' in navigator)&&('PushManager' in window)&&(typeof Notification!=='undefined'); if(!suportado){ el.innerHTML='⚠️ Seu navegador não suporta notificações push. Tenta pelo Chrome ou Safari mais recente.'; if(btn) btn.style.display='none'; return; } if(btn) btn.style.display=''; if(Notification.permission==='denied'){ el.innerHTML='🔒 As notificações estão bloqueadas pra esse site. Toque no 🔒 (ou ⓘ) ao lado do endereço no navegador → Notificações → Permitir, e depois clique no botão abaixo de novo.'; } else if(Notification.permission==='granted'){ const pend=_contarAgendamentosSemPush(); el.innerHTML = pend>0 ? '🟡 Notificações permitidas, mas você tem '+pend+' agendamento(s) futuro(s) sem aviso ativado ainda.' : '✅ Notificações ativadas — você será avisado 1h antes de cada horário marcado.'; } else { el.innerHTML='⚪ Notificações ainda não configuradas nesse navegador.'; } } function _contarAgendamentosSemPush(){ const tMid=new Date(hoje.getFullYear(),hoje.getMonth(),hoje.getDate()); return (agendamentosCliente||[]).filter(function(a){ const[d,m,y]=(a.data||'').split('/'); const dt=y?new Date(y,m-1,d):null; const st=(a.status||'').toLowerCase(); const futuro=st!=='cancelado'&&st!=='concluído'&&st!=='concluido'&&dt&&dt>=tMid; if(!futuro||!a.id) return false; let ja=false; try{ja=!!localStorage.getItem('push_ativo_ag_'+a.id);}catch(_e){} return !ja; }).length; } // Ativa o aviso push pra TODOS os agendamentos futuros de uma vez — sem // precisar clicar um por um em cada card da lista de "Próximos". async function ativarTodasNotificacoesCli(){ const btn=document.getElementById('btn-ativar-notif-todos'); const tMid=new Date(hoje.getFullYear(),hoje.getMonth(),hoje.getDate()); const pendentes=(agendamentosCliente||[]).filter(function(a){ const[d,m,y]=(a.data||'').split('/'); const dt=y?new Date(y,m-1,d):null; const st=(a.status||'').toLowerCase(); const futuro=st!=='cancelado'&&st!=='concluído'&&st!=='concluido'&&dt&&dt>=tMid; if(!futuro||!a.id) return false; let ja=false; try{ja=!!localStorage.getItem('push_ativo_ag_'+a.id);}catch(_e){} return !ja; }); if(!pendentes.length){ Swal.fire({toast:true,position:'top-end',icon:'info',title:'Nada pra ativar',text:'Todos os seus agendamentos futuros já têm aviso ativado.',showConfirmButton:false,timer:3000,background:'#0e1220',color:'#e8edf8'}); return; } if(btn){ btn.disabled=true; btn.textContent='Ativando...'; } let sucesso=0; for(const a of pendentes){ try{ await ativarNotificacaoPush(a.id, clienteEmail, clienteTelefone, null); // ativarNotificacaoPush não lança erro — checa a marca no localStorage let ok=false; try{ok=!!localStorage.getItem('push_ativo_ag_'+a.id);}catch(_e){} if(ok) sucesso++; }catch(e){ console.error('[push todos]', e); } } if(btn){ btn.disabled=false; btn.textContent='Ativar para meus agendamentos'; } _atualizarStatusNotifCli(); renderAgenCli(); if(sucesso===pendentes.length){ Swal.fire({toast:true,position:'top-end',icon:'success',title:'Notificações ativadas!',text:'Aviso ativado pra '+sucesso+' agendamento(s).',showConfirmButton:false,timer:3500,background:'#0e1220',color:'#e8edf8'}); } else if(sucesso>0){ Swal.fire({toast:true,position:'top-end',icon:'warning',title:'Parcialmente ativado',text:sucesso+' de '+pendentes.length+' agendamento(s) — o navegador pode ter bloqueado o restante.',showConfirmButton:false,timer:4000,background:'#0e1220',color:'#e8edf8'}); } else { Swal.fire({toast:true,position:'top-end',icon:'error',title:'Não deu pra ativar',text:'Verifique se as notificações estão permitidas pra esse site.',showConfirmButton:false,timer:4000,background:'#0e1220',color:'#e8edf8'}); } } function mudarTabCli(tab){ tabCli=tab; document.getElementById('tab-prox').classList.toggle('active',tab==='proximos'); document.getElementById('tab-hist').classList.toggle('active',tab==='historico'); renderAgenCli(); } // [SUBSTITUÍDO] api.run → gsr.call function carregarAgenCli(){ document.getElementById('cli-lista').innerHTML=`
Carregando seus agendamentos...
`; gsr.call('buscarAgendamentos', [clienteEmail]) .then(d => { agendamentosCliente=d||[]; renderAgenCli(); _verificarAvaliacoesPendentes(); }) .catch(() => { document.getElementById('cli-lista').innerHTML='
⚠️

Erro ao carregar. Tentar novamente

'; }); } function renderAgenCli(){ const tMid=new Date(hoje.getFullYear(),hoje.getMonth(),hoje.getDate()); const prox=[],hist=[]; agendamentosCliente.forEach((a,i)=>{ const[d,m,y]=(a.data||'').split('/'); const dt=y?new Date(y,m-1,d):null; const st=(a.status||'').toLowerCase(); if(st!=='cancelado'&&st!=='concluído'&&st!=='concluido'&&dt&&dt>=tMid)prox.push({...a,_i:i}); else hist.push({...a,_i:i}); }); prox.sort((a,b)=>_d2n(a.data)-_d2n(b.data)); hist.sort((a,b)=>_d2n(b.data)-_d2n(a.data)); document.getElementById('cnt-prox').textContent=prox.length||''; document.getElementById('cnt-hist').textContent=hist.length||''; const lista=tabCli==='proximos'?prox:hist; const el=document.getElementById('cli-lista'); if(!lista.length){ const msg=tabCli==='proximos'?'Nenhum agendamento futuro.

Agendar agora →':'Nenhum histórico ainda.'; el.innerHTML=`
${tabCli==='proximos'?'📅':'📋'}

${msg}

`; return; } el.innerHTML=lista.map(function(a){ var sc=(a.status||'').toLowerCase().replace('í','i').replace('ã','a').replace(' ','-'); var pC=['agendado','confirmado'].includes((a.status||'').toLowerCase()); var obs=a.observacao&&a.observacao!=='undefined'?a.observacao:''; var _st=(a.status||'').toLowerCase().replace('í','i'); var _c=(_st==='concluido'); var _ja=false;try{_ja=a.id?!!localStorage.getItem('aval_'+a.id):false;}catch(_e){} var btnAval=(_c&&!_ja)?'':''; var btnCancel=pC?'':''; // ── Botão pra ativar o aviso push desse agendamento específico ── // Só aparece se: navegador suporta push, o agendamento ainda está // agendado/confirmado (futuro) e ainda não foi ativado antes (marcação // fica salva no localStorage pra não repetir depois de já ter ativado). var btnNotif=''; var _pushSuportado=('serviceWorker' in navigator)&&('PushManager' in window)&&(typeof Notification!=='undefined'); var _jaAtivouPush=false;try{_jaAtivouPush=a.id?!!localStorage.getItem('push_ativo_ag_'+a.id):false;}catch(_e){} if(pC&&a.id&&_pushSuportado&&!_jaAtivouPush){ var _bloqueado=(Notification.permission==='denied'); var _corNotif=_bloqueado?'var(--muted)':'var(--accent)'; var _txtNotif=_bloqueado?'🔔 Notificação bloqueada':'🔔 Avisar 1h antes'; btnNotif=''; } var html='
'; html+='
'; html+='
'+(a.servico||'-')+' · ✂ '+(a.barbeiro||'-')+'
'; html+=''+(a.status||'-')+''; html+='
'; html+='
'; html+='📆 '+(a.data||'-')+''; html+='🕐 '+(a.horario||'-')+''; if(obs)html+='⚡ '+obs+''; html+='NN #'+(a.numero||'-')+''; html+='
'; if(btnAval||btnCancel||btnNotif)html+='
'+btnNotif+btnAval+btnCancel+'
'; html+='
'; return html; }).join(''); } function _d2n(s){if(!s)return 0;const[d,m,y]=s.split('/');return parseInt(y+m+d)||0;} function cancelarAgen(idx,numero,id){ const a=agendamentosCliente[idx]; const ident=(numero&&numero!=='null'&&numero!=='undefined'&&numero!=='')?numero:String(id||''); window._cancelPendente = { a, ident }; Swal.fire({ title:'Antes de cancelar…', html:`

Nosso barbeiro se organiza com antecedência pra te atender. Cancelamentos de última hora dificultam o atendimento de outras pessoas — cancele só se realmente não puder comparecer.

${a.servico||''} — ${a.data||''} às ${a.hora||a.horario||'—'}
`, showConfirmButton:false, showCancelButton:false, width:380 }); } const _MOTIVOS_CANCEL = [ 'Preciso remarcar para outro dia/horário', 'Não vou mais poder comparecer', 'Tive um imprevisto', 'Encontrei outra opção', 'Outro' ]; function _motivoCancelamento(){ const pend = window._cancelPendente; if(!pend) return; const opts = _MOTIVOS_CANCEL.map(function(m){ return ``; }).join(''); Swal.fire({ title:'Motivo do cancelamento', html:`

Nos ajuda a melhorar — sua resposta é opcional.

${opts}
`, showCancelButton:true, confirmButtonText:'Enviar e cancelar', cancelButtonText:'Pular e cancelar', confirmButtonColor:'#f87171', reverseButtons:true }).then(r=>{ // Fechou no X/ESC/clique fora sem escolher nada → aborta, não cancela if(!r.isConfirmed && r.dismiss!=='cancel') return; let motivo=''; if(r.isConfirmed){ const sel=document.querySelector('input[name="motivo-cancel-op"]:checked'); const outro=(document.getElementById('motivo-cancel-outro')?.value||'').trim(); motivo=outro||(sel?sel.value:''); } _executarCancelamento(pend.a, pend.ident, motivo); }); } function _executarCancelamento(a, ident, motivo){ SWL('Cancelando...'); gsr.call('cancelarAgendamentoPorNumero', [ident, motivo]) .then(res => { Swal.close(); SW('Cancelado!','Seu agendamento foi cancelado.','success'); carregarAgenCli(); }) .catch(e => { Swal.close(); SW('Erro',e?.message||'Falha ao cancelar.','error'); }); } // ══════════════════════════════════════════════════════════════ // AVALIAÇÃO DE SATISFAÇÃO (index do cliente) // ══════════════════════════════════════════════════════════════ var _avalDados = { agendamento_id: null, nota: 0 }; const _AVAL_LABELS = { 1:'😞 Péssimo', 2:'😕 Ruim', 3:'😐 Regular', 4:'😊 Bom', 5:'🤩 Excelente!' }; function abrirModalAvaliacao(agendamento) { // agendamento = objeto do agendamentosCliente _avalDados = { agendamento_id: agendamento.id || null, cliente_nome: clienteNome || agendamento.cliente_nome || '', cliente_email: clienteEmail || '', barbeiro_nome: agendamento.barbeiro || agendamento.barbeiro_nome || '', servico: agendamento.servico || '', data_servico: agendamento.data || '', nota: 0 }; const det = document.getElementById('aval-detalhe'); if (det) det.textContent = (agendamento.servico || '') + (agendamento.barbeiro ? ' com ' + agendamento.barbeiro : '') + (agendamento.data ? ' — ' + agendamento.data : ''); document.getElementById('aval-comentario').value = ''; document.getElementById('aval-btn-enviar').disabled = true; document.getElementById('aval-label').textContent = ''; _avalRenderEstrelas(0); var m = document.getElementById('avaliacaoModal'); m.style.display = 'flex'; } function fecharModalAvaliacao() { document.getElementById('avaliacaoModal').style.display = 'none'; } function avalSetNota(n) { _avalDados.nota = n; _avalRenderEstrelas(n); document.getElementById('aval-label').textContent = _AVAL_LABELS[n] || ''; document.getElementById('aval-btn-enviar').disabled = false; } function _avalRenderEstrelas(nota) { document.querySelectorAll('.aval-star').forEach(function(btn) { var v = Number(btn.getAttribute('data-v')); btn.style.opacity = v <= nota ? '1' : '0.25'; btn.style.transform = v <= nota ? 'scale(1.15)' : 'scale(1)'; }); } function enviarAvaliacao() { if (!_avalDados.nota) return; var comentario = (document.getElementById('aval-comentario').value || '').trim(); var btn = document.getElementById('aval-btn-enviar'); btn.disabled = true; btn.textContent = 'Enviando...'; gsr.call('salvarAvaliacao', [Object.assign({}, _avalDados, { comentario: comentario })]) .then(function() { fecharModalAvaliacao(); // Marcar como avaliado no localStorage para não perguntar de novo try { localStorage.setItem('aval_' + _avalDados.agendamento_id, '1'); } catch(e) {} SW('Obrigado! 🙏', 'Sua avaliação foi registrada com sucesso.', 'success'); if (typeof carregarAgenCli === 'function') carregarAgenCli(); }) .catch(function(e) { btn.disabled = false; btn.textContent = 'Enviar Avaliação'; SW('Erro', e?.message || 'Falha ao enviar avaliação.', 'error'); }); } // Verifica se há agendamentos concluídos sem avaliação e abre o modal function _verificarAvaliacoesPendentes() { if (!clienteEmail || !agendamentosCliente.length) return; const concluidos = agendamentosCliente.filter(function(a) { const st = (a.status || '').toLowerCase().replace('í','i'); return (st === 'concluido' || st.replace('í','i') === 'concluido') && a.id; }); if (!concluidos.length) return; // Verifica localmente quais já foram avaliados const pendente = concluidos.find(function(a) { try { return !localStorage.getItem('aval_' + a.id); } catch(e) { return false; } }); if (!pendente) return; // Verifica no servidor (evita duplicata) gsr.call('verificarAvaliacao', [pendente.id]) .then(function(r) { if (!r.avaliado) { // Pequeno delay para não sobrepor outros modais setTimeout(function() { abrirModalAvaliacao(pendente); }, 800); } else { try { localStorage.setItem('aval_' + pendente.id, '1'); } catch(e) {} } }) .catch(function() {}); } // ══════════════════════════════════════════════════════════════ // AVALIAÇÃO NO RODAPÉ (pública, sem login) // ══════════════════════════════════════════════════════════════ var _footerAvalNota = 0; var _FOOTER_AVAL_LABELS = {1:'😞 Péssimo',2:'😕 Ruim',3:'😐 Regular',4:'😊 Bom',5:'🤩 Excelente!'}; function footerAvalNota(n) { _footerAvalNota = n; // Renderiza estrelas document.querySelectorAll('#footer-aval-stars button').forEach(function(btn) { var v = Number(btn.getAttribute('data-v')); btn.style.opacity = v <= n ? '1' : '0.2'; btn.style.transform = v <= n ? 'scale(1.2)' : 'scale(1)'; }); document.getElementById('footer-aval-label').textContent = _FOOTER_AVAL_LABELS[n] || ''; // Mostra campo de nome só se não estiver logado var nomeWrap = document.getElementById('footer-aval-nome-wrap'); if (nomeWrap) nomeWrap.style.display = clienteNome ? 'none' : 'block'; // Mostra área de comentário + botão enviar document.getElementById('footer-aval-extra').style.display = 'block'; } function footerAvalEnviar() { if (!_footerAvalNota) return; var btn = document.getElementById('footer-aval-btn'); var comentario = (document.getElementById('footer-aval-coment').value || '').trim(); var nomeDigitado = (document.getElementById('footer-aval-nome')?.value || '').trim(); var nome = clienteNome || nomeDigitado || ''; btn.disabled = true; btn.textContent = 'Enviando...'; gsr.call('salvarAvaliacao', [{ nota: _footerAvalNota, comentario: comentario, cliente_nome: nome, cliente_email: clienteEmail || '', barbeiro_nome: '', servico: '', data_servico: '' }]) .then(function() { document.getElementById('footer-aval-form').style.display = 'none'; document.getElementById('footer-aval-ok').style.display = 'block'; // Guarda no localStorage para não pedir de novo tão cedo (24h) try { localStorage.setItem('footer_aval_ts', Date.now()); } catch(e) {} }) .catch(function() { btn.disabled = false; btn.textContent = 'Enviar Avaliação'; alert('Erro ao enviar. Tente novamente.'); }); } // Esconde o bloco se já avaliou nas últimas 24h (não irritar o usuário) (function() { try { var ts = Number(localStorage.getItem('footer_aval_ts') || 0); if (ts && (Date.now() - ts) < 86400000) { var wrap = document.getElementById('footer-aval-wrap'); if (wrap) wrap.style.display = 'none'; } } catch(e) {} })(); // ══════════════════════════════════════════════════════════════ // AVALIAÇÕES PÚBLICAS — exibição no rodapé // ══════════════════════════════════════════════════════════════ // DEPOIS: var _reviewsTodas = []; var _reviewsVisiveis = 3; // quantas mostrar inicialmente function _carregarReviewsPublicas() { gsr.call('listarAvaliacoes', [{ limite: 50 }]) .then(function(res) { _reviewsTodas = res.lista || []; var stats = res.stats || {}; var loading = document.getElementById('footer-reviews-loading'); var media = document.getElementById('footer-reviews-media'); if (loading) loading.style.display = 'none'; if (!_reviewsTodas.length) { var cont = document.getElementById('footer-reviews-lista'); if (cont) cont.innerHTML = '
Nenhuma avaliação ainda. Seja o primeiro! 😊
'; return; } if (media && stats.total) { var estrelasFill = Math.round(Number(stats.media)); media.innerHTML = '★'.repeat(estrelasFill) + '☆'.repeat(5 - estrelasFill) + ' ' + stats.media + '' + ' (' + stats.total + ' avaliações)'; media.style.color = '#eab308'; } _reviewsRenderizar(); }) .catch(function() { var loading = document.getElementById('footer-reviews-loading'); if (loading) loading.style.display = 'none'; }); } function _reviewsRenderizar() { var cont = document.getElementById('footer-reviews-lista'); var btnWrap = document.getElementById('footer-reviews-ver-mais'); if (!cont) return; var notaCor = {1:'#ef4444',2:'#f97316',3:'#eab308',4:'#84cc16',5:'#22c55e'}; var visiveis = _reviewsTodas.slice(0, _reviewsVisiveis); cont.innerHTML = visiveis.map(function(av) { var estrelas = '★'.repeat(av.nota) + ''.repeat(5 - av.nota); var nome = av.cliente_nome ? av.cliente_nome.split(' ')[0] : 'Cliente'; var data = av.criado_em ? av.criado_em.slice(0,10).split('-').reverse().join('/') : ''; var detalhe = [av.servico, av.barbeiro_nome].filter(Boolean).join(' · '); return '
' + '
' + '
' + '
' + estrelas + '
' + '
' + nome + '
' + (detalhe ? '
✂️ ' + detalhe + '
' : '') + '
' + '
' + data + '
' + '
' + (av.comentario ? '
"' + av.comentario + '"
' : '') + '
'; }).join(''); // Mostra/oculta botão "Ver mais" if (btnWrap) btnWrap.style.display = _reviewsTodas.length > _reviewsVisiveis ? 'block' : 'none'; } function _reviewsExpandir() { _reviewsVisiveis += 5; // mostra mais 5 por clique _reviewsRenderizar(); } // Carrega ao iniciar a página (function() { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', _carregarReviewsPublicas); } else { setTimeout(_carregarReviewsPublicas, 800); } })(); // Recarrega após nova avaliação enviada pelo footer var _origFooterAvalEnviar = footerAvalEnviar; footerAvalEnviar = function() { _origFooterAvalEnviar(); setTimeout(_carregarReviewsPublicas, 1500); }; // ══════════════════════════════════════════════════════════════ // RESTAURAR SESSÃO // ══════════════════════════════════════════════════════════════ // [SUBSTITUÍDO] api.run → gsr.call // CORREÇÃO: não setar clienteEmail antes do servidor validar a sessão. // Antes: clienteEmail era setado sincronamente — se alguém estava logado // no mesmo browser, o próximo cliente agendava com o email errado. (function restaurarSessao(){ const email=sessaoLer(); if(!email)return; // Não setar clienteEmail aqui — aguarda confirmação do servidor gsr.call('sessaoValida', [email]) .then(ok => { if(ok){ clienteEmail=email; entrarAreaCli(email); } else{ sessaoLimpar(); } }) .catch(() => { sessaoLimpar(); }); })(); // ══════════════════════════════════════════════════════════════ // BOTÃO WHATSAPP // ══════════════════════════════════════════════════════════════ function mostrarWhatsAppFab() { const fab = document.getElementById('whatsappFab'); if (fab) { fab.classList.add('visible'); try { localStorage.setItem('whatsapp_fab_visible_' + _slug, 'true'); } catch(e) {} } } function ocultarWhatsAppFab() { const fab = document.getElementById('whatsappFab'); if (fab) { fab.classList.remove('visible'); try { localStorage.removeItem('whatsapp_fab_visible_' + _slug); } catch(e) {} } } (function restaurarWhatsAppFab() { try { const visivel = localStorage.getItem('whatsapp_fab_visible_' + _slug); if (visivel === 'true') { setTimeout(() => mostrarWhatsAppFab(), 500); } } catch(e) {} })(); (function() { try { aplicarTema(localStorage.getItem(_TEMA_KEY_LOJA) || 'padrao'); } catch(e) {} })(); _aplicarVisualInstantaneo(); carregarTudoIniciais(); // Evita bfcache: recarrega a página se ela foi restaurada do cache do browser window.addEventListener('pageshow', function(e) { if (e.persisted) { window.location.reload(); } }); // ══════════════════════════════════════════════════════════════════ // BARBERVIP+ — Sistema de Fidelidade por Atendimento // ══════════════════════════════════════════════════════════════════ let _bcCfg = { ativo: false, meta: 10, premio: '', pontosAtendimento: true, pontosIndicacao: 0, pontosProduto: 0, modo: 'cartela' }; let _bcClientesTodos = []; // ── Carregamento principal ──────────────────────────────────────── function bcCarregar() { const nomeBarbeiro = (typeof usuarioNome !== 'undefined' && usuarioNome) || usuarioEmail || ''; if (!nomeBarbeiro) return; const lista = document.getElementById('bc-clientes-lista'); if (lista) lista.innerHTML = '
'; gsr.call('getBarberCoinClientes', nomeBarbeiro) .then(function(res) { _bcCfg = Object.assign({ ativo: false, meta: 10, premio: '', pontosAtendimento: true, pontosIndicacao: 0, pontosProduto: 0, modo: 'cartela' }, res.cfg || {}); _bcClientesTodos = res.clientes || []; _bcRenderConfig(); _bcRenderClientes(_bcClientesTodos); }) .catch(function() { toast('error', 'Erro ao carregar BarberVip+'); }); } // ── Render configuração ─────────────────────────────────────────── function _bcRenderConfig() { const toggle = document.getElementById('bc-toggle'); const metaEl = document.getElementById('bc-meta'); const premEl = document.getElementById('bc-premio'); if (!toggle) return; toggle.checked = !!_bcCfg.ativo; if (metaEl) metaEl.value = _bcCfg.meta || 10; if (premEl) premEl.value = _bcCfg.premio || ''; // Fontes de pontos const fonteAtend = document.getElementById('bc-fonte-atendimento'); const fonteInd = document.getElementById('bc-fonte-indicacao'); const fonteProd = document.getElementById('bc-fonte-produto'); const trackAtend = document.getElementById('bc-fonte-atendimento-track'); const thumbAtend = document.getElementById('bc-fonte-atendimento-thumb'); const paAtivo = _bcCfg.pontosAtendimento !== false; if (fonteAtend) fonteAtend.checked = paAtivo; if (trackAtend) trackAtend.style.background = paAtivo ? '#f5a623' : 'var(--border)'; if (thumbAtend) thumbAtend.style.left = paAtivo ? '21px' : '3px'; if (fonteInd) fonteInd.value = _bcCfg.pontosIndicacao || 0; if (fonteProd) fonteProd.value = _bcCfg.pontosProduto || 0; _bcAtualizarToggleVisual(_bcCfg.ativo); bcAtualizarPreview(); } function _bcAtualizarToggleVisual(ativo) { const track = document.getElementById('bc-toggle-track'); const thumb = document.getElementById('bc-toggle-thumb'); const badge = document.getElementById('bc-status-badge'); const fields = document.getElementById('bc-config-fields'); if (track) track.style.background = ativo ? '#f5a623' : 'var(--border)'; if (thumb) thumb.style.left = ativo ? '23px' : '3px'; if (badge) { badge.textContent = ativo ? '● Ativo' : '○ Inativo'; badge.style.background = ativo ? 'rgba(245,166,35,0.15)' : 'var(--surface2)'; badge.style.color = ativo ? '#f5a623' : 'var(--muted)'; badge.style.border = ativo ? '1px solid rgba(245,166,35,0.4)' : '1px solid var(--border)'; } if (fields) fields.style.display = ativo ? 'flex' : 'none'; } function bcToggleAtivo(el) { _bcAtualizarToggleVisual(el.checked); bcAtualizarPreview(); } // ── Preview da cartela ──────────────────────────────────────────── function bcAtualizarPreview() { const metaEl = document.getElementById('bc-meta'); const premEl = document.getElementById('bc-premio'); const previewEl = document.getElementById('bc-preview-stamps'); const labelEl = document.getElementById('bc-preview-meta-label'); if (!previewEl) return; const meta = Math.max(1, parseInt((metaEl && metaEl.value) || 10) || 10); const premio = (premEl && premEl.value) || '🎁 Prêmio'; const exemplo = Math.min(3, meta); let html = ''; for (let i = 0; i < meta; i++) { const cheio = i < exemplo; html += '
' + (cheio ? '🪙' : '') + '
'; } previewEl.innerHTML = html; if (labelEl) { labelEl.innerHTML = '' + meta + ' atendimentos' + ' = ' + premio + ''; } } // ── Salvar configuração ─────────────────────────────────────────── function bcSalvarConfig() { const nomeBarbeiro = (typeof usuarioNome !== 'undefined' && usuarioNome) || usuarioEmail || ''; const ativo = document.getElementById('bc-toggle').checked; const meta = parseInt(document.getElementById('bc-meta').value) || 10; const premio = (document.getElementById('bc-premio').value || '').trim(); const pontosAtendimento = !!(document.getElementById('bc-fonte-atendimento') || { checked: true }).checked; const pontosIndicacao = parseInt((document.getElementById('bc-fonte-indicacao') || {}).value || 0) || 0; const pontosProduto = parseInt((document.getElementById('bc-fonte-produto') || {}).value || 0) || 0; if (ativo && !premio) { toast('warning', 'Informe a descrição do prêmio!'); document.getElementById('bc-premio').focus(); return; } if (meta < 1) { toast('warning', 'Meta mínima: 1 atendimento.'); return; } swalL('Salvando...'); gsr.call('salvarBarberCoinConfig', [nomeBarbeiro, ativo, meta, premio, { pontosAtendimento, pontosIndicacao, pontosProduto }]) .then(function() { Swal.close(); _bcCfg = { ativo, meta, premio, pontosAtendimento, pontosIndicacao, pontosProduto }; _bcRenderConfig(); toast('success', '🪙 BarberVip+ salvo!'); }) .catch(function() { Swal.close(); toast('error', 'Erro ao salvar'); }); } // ── Render lista de clientes ────────────────────────────────────── function _bcRenderClientes(lista) { const container = document.getElementById('bc-clientes-lista'); if (!container) return; if (!lista || !lista.length) { container.innerHTML = '
' + '🪙 Nenhum cliente com pontos ainda.
' + 'Os pontos são adicionados automaticamente quando um atendimento é marcado como ' + 'Concluído.
'; return; } const meta = _bcCfg.meta || 10; container.innerHTML = lista.map(function(c) { const pct = Math.min(100, Math.round((c.pontos / meta) * 100)); const pronto = c.pontos >= meta; const stamps = _bcStamps(c.pontos, meta); return '
' + '
' + '
' + '
' + (c.nome||'Sem nome') + '
' + '
📱 ' + (c.tel||'—') + (c.resgatados ? '  ·  🎁 ' + c.resgatados + 'x resgatado' : '') + '
' + '
' + (pronto ? '
🎉 PRONTO
' : '
' + c.pontos + ' / ' + meta + '
' ) + '
' + '
' + stamps + '
' + '
' + '
' + '
' + (pronto ? '' : '' ) + '
' + '' + '' + '' + '
' + '
'; }).join(''); } function _bcStamps(pontos, meta) { let html = ''; for (let i = 0; i < meta; i++) { const cheio = i < pontos; html += '
' + (cheio ? '🪙' : '') + '
'; } return html; } // ── Filtro de busca ─────────────────────────────────────────────── function bcFiltrarClientes(termo) { const t = (termo || '').toLowerCase().trim(); if (!t) { _bcRenderClientes(_bcClientesTodos); return; } _bcRenderClientes(_bcClientesTodos.filter(function(c) { return (c.nome||'').toLowerCase().includes(t) || (c.tel||'').replace(/D/g,'').includes(t.replace(/D/g,'')); })); } // ── Resgatar prêmio ─────────────────────────────────────────────── function bcResgatar(clienteTel, clienteNome) { const nomeBarbeiro = (typeof usuarioNome !== 'undefined' && usuarioNome) || usuarioEmail || ''; const premio = _bcCfg.premio || 'prêmio'; Swal.fire({ title: '🎁 Confirmar resgate', html: '
' + 'Cliente: ' + clienteNome + '
' + 'Prêmio: ' + premio + '

' + 'Confirma que o prêmio foi entregue?' + '
', icon: 'question', background: '#0e1220', color: '#e8edf8', showCancelButton: true, confirmButtonText: '✅ Sim, confirmar', cancelButtonText: 'Cancelar', confirmButtonColor: '#f5a623', }).then(function(r) { if (!r.isConfirmed) return; swalL('Registrando...'); gsr.call('resgatarBC', clienteTel, nomeBarbeiro) .then(function(res) { Swal.close(); if (res.ok) { Swal.fire({ title: '🎉 Resgate registrado!', html: '
' + '' + clienteNome + ' recebeu: ' + '' + premio + '

' + (res.pontos > 0 ? 'Pontos restantes: ' + res.pontos + ' 🪙' : '✨ Cartela zerada — nova rodada iniciada!') + '
', icon: 'success', background: '#0e1220', color: '#e8edf8', confirmButtonColor: '#f5a623', }); if (typeof bcCarregar === 'function') bcCarregar(); } else { toast('warning', res.motivo || 'Erro ao resgatar'); } }) .catch(function() { Swal.close(); toast('error', 'Erro ao resgatar'); }); }); } // ── Notificação toast ao concluir atendimento ───────────────────── function bcNotificarPonto(agen) { if (!agen) return; const nomeBarbeiro = agen.barbeiro || ''; const tel = agen.telefone || ''; if (!nomeBarbeiro || !tel) return; // Aguarda o backend processar e então busca os pontos atualizados setTimeout(function() { gsr.call('getBarberCoinPontos', [tel, nomeBarbeiro]) .then(function(res) { if (!res || !res.ativo) return; const atingiu = res.pontos >= res.meta; Swal.fire({ toast: true, position: 'bottom-end', showConfirmButton: false, timer: atingiu ? 7000 : 4000, timerProgressBar: true, background: atingiu ? '#1a1000' : 'var(--surface)', html: '
' + '
' + (atingiu ? '🎉' : '🪙') + '
' + '
' + '
' + (atingiu ? 'Prêmio conquistado!' : 'BarberVip+ +1') + '
' + '
' + (agen.nome || 'Cliente') + ' · ' + res.pontos + ' / ' + res.meta + ' 🪙' + '
' + (atingiu ? '
' + (res.premio || '') + '
' : '') + '
' + '
', didOpen: function(el) { el.style.border = '1px solid ' + (atingiu ? 'rgba(245,166,35,0.6)' : 'var(--border)'); el.style.borderRadius = '10px'; } }); // Recarrega a aba se estiver aberta if (typeof paginaAtual !== 'undefined' && paginaAtual === 'barbercoin' && typeof bcCarregar === 'function') bcCarregar(); }) .catch(function() {}); }, 1500); } function togglePerfil() { const f = document.getElementById('cli-form-perfil'); if (!f) return; const abrindo = f.style.display === 'none'; // Fecha o formulário de senha se estiver aberto if (abrindo) { const fs = document.getElementById('cli-form-senha'); if (fs && fs.style.display !== 'none') { fs.style.display = 'none'; ['s-atual','s-nova','s-confirma'].forEach(id => { const el=document.getElementById(id); if(el) el.value=''; }); const se = document.getElementById('s-err'); if(se) se.style.display='none'; } } f.style.display = abrindo ? 'block' : 'none'; if (abrindo) { // Preenche com dados atuais const nomeEl = document.getElementById('perf-nome'); const telEl = document.getElementById('perf-telefone'); if (nomeEl) nomeEl.value = clienteNome || ''; if (telEl) telEl.value = clienteTelefone || ''; // Limpa campos de senha ['perf-senha-atual','perf-senha-nova','perf-senha-confirma'].forEach(function(id) { const el = document.getElementById(id); if (el) el.value = ''; }); const err = document.getElementById('perf-err'); if (err) err.style.display = 'none'; } } function clienteSalvarPerfil() { const nome = (document.getElementById('perf-nome')?.value || '').trim(); const telefone = (document.getElementById('perf-telefone')?.value || '').replace(/D/g,''); const senhaAtual = document.getElementById('perf-senha-atual')?.value || ''; const novaSenha = document.getElementById('perf-senha-nova')?.value || ''; const confirmaSenha = document.getElementById('perf-senha-confirma')?.value || ''; const errEl = document.getElementById('perf-err'); if (!nome) { if (errEl) { errEl.textContent = 'Informe o nome.'; errEl.style.display = 'block'; } return; } if (novaSenha && novaSenha.length < 6) { if (errEl) { errEl.textContent = 'A nova senha deve ter pelo menos 6 caracteres.'; errEl.style.display = 'block'; } return; } if (novaSenha && novaSenha !== confirmaSenha) { if (errEl) { errEl.textContent = 'As senhas não conferem.'; errEl.style.display = 'block'; } return; } if (novaSenha && !senhaAtual) { if (errEl) { errEl.textContent = 'Informe a senha atual para alterá-la.'; errEl.style.display = 'block'; } return; } if (errEl) errEl.style.display = 'none'; swalL('Salvando...'); gsr.call('atualizarPerfilCliente', [{ email: clienteEmail, nome: nome, telefone: telefone, senhaAtual: senhaAtual || '', novaSenha: novaSenha || '' }]).then(function(res) { Swal.close(); if (res && res.sucesso) { clienteNome = nome; clienteTelefone = telefone; document.getElementById('cli-nome-display').textContent = nome; document.getElementById('cli-form-perfil').style.display = 'none'; toast('success', '✅ Perfil atualizado!'); // Recarrega BarberVip+ agora que pode ter telefone novo _carregarBcCliente(); } else { if (errEl) { errEl.textContent = (res && res.erro) || 'Erro ao salvar.'; errEl.style.display = 'block'; } } }).catch(function(e) { Swal.close(); if (errEl) { errEl.textContent = e && e.message ? e.message : 'Erro ao salvar.'; errEl.style.display = 'block'; } }); }